Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- An MCP tool call whose `projectPath` names a file inside a project, such as the file an agent is looking at, now answers from that project. Before, it failed with "Path is not a directory", even though a path that doesn't exist at all worked. Sensitive folders such as `/etc` are still refused when the path names a file inside them. Thanks @inth3shadows for the report. (#2982)
- In TypeScript, an awaited call with type arguments like `await fetchApi<User>("/me")` or `await api.get<T>(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586)
- In JavaScript and TypeScript, a call or `new` in a parameter's default value, like `function renderWithNote(note = buildNote())`, a destructured `{ size = defaultSize() }` or `constructor(private svc = new Service())`, now counts as the function's own call. Before, it was dropped, so `buildNote` was missing that caller and impact on it came back incomplete. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and the pull request. (#2588)
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
Expand Down
13 changes: 13 additions & 0 deletions __tests__/mcp-nested-repo-projectpath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ describe('projectPath inside a nested git repository the ancestor index excludes
expect(text(res)).toContain('parent-only.ts');
});

it('gives the same #155 notice when projectPath names a file in that worktree (#2982)', async () => {
const wt = path.join(parent, 'wt');
git(parent, 'worktree', 'add', '-q', '-b', 'feature', wt);
handler = new ToolHandler(cg);
const res = await handler.execute('codegraph_search', {
query: 'parentOnly',
projectPath: path.join(wt, 'src', 'sub.ts'),
});
expect(res.isError).toBeUndefined();
expect(text(res)).toContain('different git worktree');
expect(text(res)).toContain('parent-only.ts');
});

it.runIf(process.platform === 'win32')('serves that linked worktree spelled with a lowercase drive letter too (#2517)', async () => {
// VS Code and Cursor pass `d:\…` while git answers `D:\…`: one directory, so
// the worktree is still the borrowed-index case, never a repository of its own.
Expand Down
57 changes: 57 additions & 0 deletions __tests__/mcp-projectpath-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* A `projectPath` that names a file inside an indexed project (#2982).
*
* Passing the file it is looking at is an easy agent mistake. It used to be
* refused with `isError: true` ("Path is not a directory") while a path that
* doesn't exist at all walked up to the project's `.codegraph/` fine. A file
* now resolves through its directory like any other path inside the project,
* and the sensitive-folder refusal still applies to a file inside one.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { ToolHandler, __setLoadCodeGraphForTests } from '../src/mcp/tools';

describe('projectPath naming a file (#2982)', { timeout: 30_000 }, () => {
let project: string;
let handler: ToolHandler | undefined;

beforeAll(async () => {
project = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-2982-')));
fs.mkdirSync(path.join(project, 'src'));
fs.writeFileSync(path.join(project, 'src', 'a.ts'), 'export function fileTarget() { return 1; }\n');
const cg = await CodeGraph.init(project, { config: { include: ['**/*.ts'], exclude: [] } });
await cg.indexAll();
cg.close();
__setLoadCodeGraphForTests(CodeGraph);
});

afterAll(async () => {
__setLoadCodeGraphForTests(null);
if (handler) await handler.closeAll();
fs.rmSync(project, { recursive: true, force: true });
});

it('resolves a file inside the project to that project', async () => {
handler = new ToolHandler(null);
const res = await handler.execute('codegraph_search', {
query: 'fileTarget',
projectPath: path.join(project, 'src', 'a.ts'),
});

expect(res.isError).toBeUndefined();
expect(res.content[0]!.text).toMatch(/fileTarget/);
});

it.runIf(process.platform !== 'win32')('still refuses a file inside a sensitive folder', async () => {
const res = await new ToolHandler(null).execute('codegraph_search', {
query: 'anything',
projectPath: '/etc/hosts',
});

expect(res.isError).toBe(true);
expect(res.content[0]!.text).toMatch(/sensitive/);
});
});
13 changes: 11 additions & 2 deletions src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ function wslSharedIndexGuidance(err: WslSharedIndexError): string {
export { PathRefusalError } from '../errors';
import { PathRefusalError } from '../errors';
import { indexedHashInput } from '../file-limits';
import { resolve as resolvePath, relative as relativePath } from 'path';
import { resolve as resolvePath, relative as relativePath, dirname } from 'path';

/** Maximum output length to prevent context bloat (characters) */
const MAX_OUTPUT_LENGTH = 15000;
Expand Down Expand Up @@ -2941,7 +2941,16 @@ export class ToolHandler {
// serve it. Resolved on the main thread so the watcher lives here even
// when dispatch is off-loaded to a worker.
if (typeof pathCheck === 'string') {
await this.awaitProjectGate(pathCheck);
// A file inside a project (an agent passing the file it is looking at)
// stands for its directory, like any other path inside it (#2982).
// Rewritten here so the gate, the worker, the worktree notice and
// getCodeGraph all see the same directory.
let projectPath = pathCheck;
try {
if (statSync(pathCheck).isFile()) projectPath = dirname(pathCheck);
} catch { /* a missing path walks up to its .codegraph/ as before */ }
if (projectPath !== pathCheck) args = { ...args, projectPath };
await this.awaitProjectGate(projectPath);
}
// The `path` and `pattern` properties used by codegraph_files are
// also path-shaped — apply the same cap.
Expand Down