1
0

mcp-require-project-path.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. /**
  2. * No-default-project → projectPath is `required` in the tool schema (issue #993).
  3. *
  4. * When the MCP server has no default project to fall back to — a gateway server
  5. * started outside any repo, or a monorepo root whose `.codegraph/` indexes live
  6. * only in sub-projects — every tool call MUST carry an explicit `projectPath`.
  7. * `ToolHandler.getTools()` reflects that by marking `projectPath` required in the
  8. * exposed schemas, a high-salience nudge that gets the agent to pass it on the
  9. * first call instead of omitting it (the reported behavior). When a default
  10. * project IS open, projectPath stays optional: a bare call falls back to it.
  11. *
  12. * The change is schema-only — the runtime stays exactly as before: a missing
  13. * projectPath with no default still returns SUCCESS-shaped guidance (never
  14. * `isError`), and a missing projectPath WITH a default still falls back to it.
  15. */
  16. import { describe, it, expect, afterEach, beforeEach } from 'vitest';
  17. import * as fs from 'fs';
  18. import * as path from 'path';
  19. import * as os from 'os';
  20. import { ToolHandler, tools } from '../src/mcp/tools';
  21. import { CodeGraph } from '../src';
  22. const ENV = 'CODEGRAPH_MCP_TOOLS';
  23. const exploreOf = (defs: { name: string; inputSchema: { required?: string[] } }[]) =>
  24. defs.find((t) => t.name === 'codegraph_explore')!;
  25. describe('No-default-project requires projectPath in the schema (#993)', () => {
  26. const originalAllowlist = process.env[ENV];
  27. afterEach(() => {
  28. if (originalAllowlist === undefined) delete process.env[ENV];
  29. else process.env[ENV] = originalAllowlist;
  30. });
  31. it('marks projectPath required on codegraph_explore when no default project is loaded', () => {
  32. const explore = exploreOf(new ToolHandler(null).getTools());
  33. expect(explore.inputSchema.required).toContain('projectPath');
  34. // The tool's own required arg is preserved, not replaced.
  35. expect(explore.inputSchema.required).toContain('query');
  36. });
  37. it('requires projectPath on EVERY exposed tool, incl. ones with no prior required list', () => {
  38. // status has no `required` array of its own → it should gain ['projectPath'].
  39. process.env[ENV] = 'explore,node,status';
  40. const got = new ToolHandler(null).getTools();
  41. expect(got.map((t) => t.name).sort()).toEqual([
  42. 'codegraph_explore',
  43. 'codegraph_node',
  44. 'codegraph_status',
  45. ]);
  46. for (const t of got) {
  47. expect(t.inputSchema.required ?? []).toContain('projectPath');
  48. }
  49. });
  50. it('does NOT mutate the shared module-level tools array (purity)', () => {
  51. // Marking required must clone — otherwise a no-default session would corrupt
  52. // the schema every later default-project session reuses.
  53. new ToolHandler(null).getTools();
  54. expect(exploreOf(tools).inputSchema.required).toEqual(['query']);
  55. });
  56. it('a missing projectPath with no default is still SUCCESS-shaped guidance, not isError', async () => {
  57. // Schema-only change: the runtime backstop is unchanged. A client that
  58. // ignores `required` still gets the nudge, never a session-souring isError.
  59. const res = await new ToolHandler(null).execute('codegraph_explore', { query: 'anything' });
  60. expect(res.isError).toBeUndefined();
  61. expect(res.content[0]!.text).toMatch(/No CodeGraph project is loaded/);
  62. expect(res.content[0]!.text).toMatch(/projectPath/);
  63. });
  64. });
  65. describe('A default project keeps projectPath OPTIONAL (#993)', () => {
  66. let tempDir: string;
  67. let cg: CodeGraph;
  68. beforeEach(async () => {
  69. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-reqpath-'));
  70. fs.writeFileSync(
  71. path.join(tempDir, 'pay.ts'),
  72. 'export function processPayment(amount: number): boolean { return amount > 0; }\n'
  73. );
  74. cg = await CodeGraph.init(tempDir, { index: true });
  75. });
  76. afterEach(() => {
  77. cg.close();
  78. fs.rmSync(tempDir, { recursive: true, force: true });
  79. });
  80. it('leaves projectPath optional when a default project is loaded', () => {
  81. const explore = exploreOf(new ToolHandler(cg).getTools());
  82. expect(explore.inputSchema.required).toEqual(['query']);
  83. expect(explore.inputSchema.required).not.toContain('projectPath');
  84. });
  85. it('a bare call (no projectPath) still falls back to the default project', async () => {
  86. const res = await new ToolHandler(cg).execute('codegraph_explore', { query: 'processPayment' });
  87. expect(res.isError).toBeUndefined();
  88. // Resolved against the default project — not the no-default guidance.
  89. expect(res.content[0]!.text).not.toMatch(/No CodeGraph project is loaded/);
  90. expect(res.content[0]!.text).toMatch(/processPayment/);
  91. });
  92. });