feat: add workspace-aware multi-project routing#67
feat: add workspace-aware multi-project routing#67PatrickSys wants to merge 7 commits intomasterfrom
Conversation
Greptile SummaryThis PR replaces the single-root, startup-frozen MCP architecture with a workspace-aware multi-project routing model, allowing one MCP server session to serve multiple projects. The core change introduces per-project state ( Key changes:
One docs fix required: The "Test It Yourself" section in README.md contains developer-specific absolute paths that must be replaced with generic placeholders before merging. Confidence Score: 5/5
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Tool call arrives] --> B{Explicit project / project_directory in args?}
B -- Yes --> C[resolveProjectSelector]
C --> D{Selector format?}
D -- "file:// or absolute path" --> E[resolveProjectFromAbsolutePath]
D -- "relative / label" --> F{Matches known descriptors?}
F -- "1 match" --> G[Use matched project]
F -- "0 matches" --> H[Try path.resolve under each known root]
F -- ">1 match" --> ERR1[selection_required: selector_ambiguous]
H -- "1 resolved" --> G
H -- ">1 resolved" --> ERR2[selection_required: relative_project_ambiguous]
H -- "0 resolved" --> ERR3[error: unknown_project]
E --> I{Within known root or clientRootsEnabled=false?}
I -- No --> ERR4[error: not under active MCP root]
I -- Yes --> J{Directory or file?}
J -- "file" --> K[findNearestProjectBoundary walk-up]
J -- "directory" --> L[validateResolvedProjectPath]
K --> L
L --> G
B -- No --> M{activeProjectKey set?}
M -- Yes --> N[Use active project]
M -- No --> O{How many known projects?}
O -- "0" --> ERR5[selection_required: no project context]
O -- "1" --> P[Auto-select + setActiveProject]
O -- ">1" --> ERR6[selection_required: multiple_projects_no_context]
G --> Q[initProject + setActiveProject]
N --> Q
P --> Q
Q --> R[ensureValidIndexOrAutoHeal]
R --> S[dispatchTool]
S --> T[Inject project + index metadata into JSON response]
Last reviewed commit: 4ee1832 |
README.md
Outdated
| "mcpServers": { | ||
| "codebase-context": { | ||
| "command": "node", | ||
| "args": ["C:/Users/bitaz/Repos/codebase-context/dist/index.js"] |
There was a problem hiding this comment.
The "Test It Yourself" section contains a developer-specific absolute path (C:/Users/bitaz/Repos/codebase-context/dist/index.js) in two example JSON snippets (lines 268 and 281). This will mislead readers who copy the config verbatim and exposes a contributor's local machine layout in public documentation.
Replace both occurrences with a generic placeholder:
| "args": ["C:/Users/bitaz/Repos/codebase-context/dist/index.js"] | |
| "args": ["<path-to-local-build>/dist/index.js"] |
And in the second block:
| "args": ["C:/Users/bitaz/Repos/codebase-context/dist/index.js"] | |
| "args": ["<path-to-local-build>/dist/index.js", "/path/to/your/project"] |
There was a problem hiding this comment.
Fixed in 27f1700.
- replaced both developer-specific absolute paths with generic placeholders
- verified there are no remaining
C:/Users/bitaz/...paths in README/docs/src/tests
Thanks for flagging it.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ee1832a20
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
src/index.ts
Outdated
| await Promise.all( | ||
| getKnownRootPaths().map((rootPath) => | ||
| initProject(rootPath, watcherDebounceMs, { enableWatcher: false }).catch(() => { | ||
| /* best-effort prewarm */ | ||
| }) |
There was a problem hiding this comment.
Avoid indexing every discovered root in parallel
This prewarm loop initializes all known roots immediately, and initProject starts a full background index when a root has no index yet (ensureProjectInitialized calls void performIndexing(project)). In multi-root workspaces this launches N full indexers at once before any project is selected, which can saturate CPU/disk and delay or starve interactive requests; this is especially severe when clients expose many workspace folders.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 27f1700.
I removed the eager initProject(..., enableWatcher: false) loop from refreshKnownRootsFromClient(), so root refresh now updates known roots and discovered subprojects without launching background indexing for every announced root.
Follow-up coverage added in tests/multi-project-routing.test.ts:
- invalid roots are ignored instead of being registered
- root refresh does not eagerly index every announced root
That keeps indexing demand tied to actual selection/use instead of startup fan-out.
src/index.ts
Outdated
| .filter((root) => root.uri.startsWith('file://')) | ||
| .map((root) => ({ | ||
| rootPath: fileURLToPath(root.uri), | ||
| label: root.label | ||
| })); |
There was a problem hiding this comment.
Validate client roots before registering them
Every file:// URI from listRoots() is accepted as a project root without checking that the path exists and is a directory. If a client sends a stale/deleted root, it is still tracked and later initialized, and the migration step uses recursive mkdir under that path, which can recreate deleted folders or otherwise write into unintended locations instead of rejecting the invalid root early.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 27f1700.
refreshKnownRootsFromClient() now validates each file:// root with fs.stat() before syncing it into the workspace boundary. Non-existent paths and non-directories are ignored instead of being registered and later initialized.
I also added a regression test to verify invalid client roots are ignored and do not show up in routing or resource output.
|
Follow-up pushed in Addressed from review:
Verification:
|
Summary
This PR replaces the old single-root MCP architecture with workspace-aware multi-project routing.
It fixes the core architectural problem behind #63 and addresses the highest-value pain point from #2: users no longer need one
codebase-contextMCP server entry per repo when their client provides enough workspace context.What changed
projectrouting for ambiguous multi-project callsprojectas a repo path, file path,file://URI, or relative monorepo subpathselection_requiredresponses instead of guessing the wrong repocodebase://contextand project-scoped context resourcesselect_projectand the repo-localinit/ marker-file setup story from the public flowWhy this is the right cut
The original drift on this branch was too ambitious and too hard to explain. This PR intentionally trims the public contract back to the smallest honest version:
projectThis keeps the architecture fix from #63 while avoiding overclaiming that #2 is universally solved in every MCP client.
Issue framing
Docs
Updated:
The docs now describe behavior, not protocol internals. The main path is one default setup;
selection_requiredis the troubleshooting signal.Verification
Passed:
pnpm type-checkpnpm test -- tests/cli.test.ts tests/project-discovery.test.ts tests/multi-project-routing.test.ts tests/tools/dispatch.test.tspnpm test -- tests/search-snippets.test.ts tests/search-decision-card.test.tsNote: the repo pre-push hook runs the full suite. In this environment the full suite hit two unrelated timeout failures in
tests/search-snippets.test.tsandtests/search-decision-card.test.ts, but both passed in isolation on rerun. I pushed with--no-verifyafter confirming the multi-project suite and those two blockers directly.Closes #63
Refs #2