Skip to content

fix(mcp): a projectPath naming a file resolves through its directory - #3075

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-2996-projectpath-file
Oct 30, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-2996-projectpath-file

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

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 got isError: true ("Path is not a directory"), while a path that doesn't exist at all walked up to the project's .codegraph/ fine. An error like that teaches an agent to stop calling codegraph, and this mistake is recoverable (AGENTS.md, "Errors teach abandonment").

Lands #2996 by @maxmilian.

Root cause

getCodeGraph runs validateProjectPath on any path that exists. Besides the sensitive-folder check, that function requires a directory, so an existing file was refused as a security-style PathRefusalError.

The fix (from @maxmilian, verbatim)

Right after the length check, execute() takes an existing file to stand for its directory and rewrites args.projectPath there. The project gate, the worker dispatch, the #155 worktree notice and getCodeGraph then all see that directory. A path that can't be stat'd walks up as before.

Path safety

A file reaches exactly what its directory reaches, and never more. The directory goes through every check any projectPath gets: the sensitive-folder refusal on the path itself when it exists, and on the root its up-walk lands on (#2929, #1930; #3012 left this split as sensitiveDirectoryRefusal). The directory is also a path the agent could pass itself, so no index is reachable that wasn't before.

  • /etc/hosts is refused as /etc is, a file in ~/.ssh as ~/.ssh is, and C:\Windows\win.ini as C:\Windows is.
  • A symlink inside a project that points at a file in a sensitive folder is answered from the link's own folder, as a symlinked folder already is. Nothing in the sensitive folder is opened. The contributor flagged this in the PR, and it is the one case that main refused and this answers.
  • Refusals stay isError; everything else stays success-shaped.

What we added

  • A test of ours, answers a file exactly as it answers the folder holding it. It asks for a file and for its directory in 4 cases and requires the same answer for both, isError and text alike: an indexed project, ~/.ssh (in the sandboxed home), a folder in no project, and, on Windows, C:\Windows\win.ini.
  • The CHANGELOG bullet moved from the top of ### Fixes to beside the other projectPath bullet, with the contributor credited.

Validation (Windows 11, main de0a4eb)

call main this PR
codegraph_search, projectPath: <project>\src\a.ts isError: Path is not a directory the project's results
codegraph_status, same path isError: Path is not a directory the project's status
codegraph_search, projectPath: C:\Windows\win.ini isError: Path is not a directory isError: Refusing to operate on sensitive system directory: C:\Windows
  • Full suite on this exact commit: CODEGRAPH_KERNEL_EXPECT=1 npx vitest run --maxWorkers=4 --minWorkers=1, with CODEGRAPH_NO_DAEMON unset: Test Files 813 passed | 1 failed | 6 skipped (820); Tests 9638 passed | 1 failed | 87 skipped (9726). The one failure, mcp-daemon.test.ts > "a proxy serving in-process hands the writer lock back to a fresh daemon (A proxy that falls back to in-process serving keeps the project's writer lock for life #2277)", is the known daemon-timing flake under full-suite load ("Daemon candidates … were still running"). The file passes alone, twice, on this commit.

Fixes #2982

Lands #2996 by @maxmilian

🤖 Generated with Claude Code

An agent that passed the file it was looking at as `projectPath` got
`isError: true` ("Path is not a directory"), while a path that doesn't
exist at all walked up to the project's `.codegraph/` fine. An error teaches
an agent to stop calling codegraph, and this mistake is recoverable.

`execute()` now takes an existing file to stand for its directory, right
after the length check, and rewrites `args.projectPath` there, so the
project gate, the worker dispatch, the worktree notice and getCodeGraph all
see that directory. A path that can't be stat'd walks up as before.

Path safety: the directory goes through the same checks as any
projectPath, the sensitive-folder refusal on the path itself and on the
root its up-walk lands on, so a file reaches exactly what its directory
reaches and never more. `/etc/hosts` is refused as `/etc` is, a file in
`~/.ssh` as `~/.ssh` is, and `win.ini` in the Windows folder as that folder
is. A symlink inside a project that points at a file in a sensitive folder
is answered from the link's own folder, as a symlinked folder already is;
nothing in the sensitive folder is opened.

Validation on main de0a4eb (Windows 11):
- Red/green: the PR's 2 tests (a file in an indexed project, and a file in
  a linked worktree, which keeps its worktree notice) and 1 of ours fail
  with main's tools.ts and pass with the fix. Ours asks for a file and for
  its directory in 4 cases, an indexed project, `~/.ssh`, a folder in no
  project, and the Windows folder's `win.ini`, and requires the same
  answer for both, refusals included.
- A real `serve --mcp` session over stdio with a private HOME: on main,
  codegraph_search and codegraph_status for `<project>/src/a.ts` both
  returned isError "Path is not a directory"; with the fix both answer from
  the project, and `win.ini` is refused as a sensitive system directory.

Original PR #2996 by @maxmilian.
Fixes #2982

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp: a projectPath naming a file inside a project returns isError ("Path is not a directory")

2 participants