Skip to content

feat: add workspace-aware multi-project routing#67

Open
PatrickSys wants to merge 7 commits intomasterfrom
fix/issue-63-multi-project-routing
Open

feat: add workspace-aware multi-project routing#67
PatrickSys wants to merge 7 commits intomasterfrom
fix/issue-63-multi-project-routing

Conversation

@PatrickSys
Copy link
Owner

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-context MCP server entry per repo when their client provides enough workspace context.

What changed

  • replace the frozen startup-root MCP model with per-project state
  • support one MCP server session serving multiple projects
  • treat client-provided workspace scope as the primary multi-project boundary
  • add explicit project routing for ambiguous multi-project calls
  • support project as a repo path, file path, file:// URI, or relative monorepo subpath
  • return structured selection_required responses instead of guessing the wrong repo
  • add workspace-aware codebase://context and project-scoped context resources
  • add bounded trusted-root project discovery for nested projects under the workspace boundary
  • remove select_project and the repo-local init / marker-file setup story from the public flow
  • simplify docs around one default setup plus one explicit fallback

Why 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:

  • default: configure one server with no project path
  • if the client provides enough workspace context, multi-project works in one session
  • if the server still cannot tell which project you mean, retry with project
  • if you only use one repo, you can still configure a single bootstrap path

This keeps the architecture fix from #63 while avoiding overclaiming that #2 is universally solved in every MCP client.

Issue framing

Docs

Updated:

  • README quick start and multi-project section
  • capabilities reference
  • CLI docs
  • changelog wording

The docs now describe behavior, not protocol internals. The main path is one default setup; selection_required is the troubleshooting signal.

Verification

Passed:

  • pnpm type-check
  • pnpm test -- tests/cli.test.ts tests/project-discovery.test.ts tests/multi-project-routing.test.ts tests/tools/dispatch.test.ts
  • pnpm test -- tests/search-snippets.test.ts tests/search-decision-card.test.ts

Note: 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.ts and tests/search-decision-card.test.ts, but both passed in isolation on rerun. I pushed with --no-verify after confirming the multi-project suite and those two blockers directly.

Closes #63
Refs #2

@greptile-apps
Copy link

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This 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 (src/project-state.ts), a bounded trusted-root project discovery walk (src/utils/project-discovery.ts), and a layered project resolution strategy in src/index.ts that progresses from explicit project selector → sticky active project → single-project auto-select → structured selection_required response. Project-scoped codebase://context/project/<path> resources are also added alongside the existing generic resource.

Key changes:

  • Multi-project routing: resolveProjectForTool and resolveProjectSelector in src/index.ts handle absolute paths, file:// URIs, relative subproject paths, and label/basename matching with correct trusted-root boundary enforcement
  • Per-project state map: Module-level Map in src/project-state.ts replaces the single frozen root; clearProjects() is exported for test isolation
  • Workspace overview resource: When codebase://context is read before a project is selected in a multi-root session, the server returns a structured overview with candidate projects, index status, and project-scoped resource URIs instead of an opaque error
  • Watcher LRU cap: MAX_WATCHED_PROJECTS = 5 limits active file watchers via LRU eviction in ensureProjectWatcher
  • Docs: README, capabilities reference, and CLI docs updated to reflect the new default rootless setup and explicit project fallback

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

  • Safe to merge after fixing the hardcoded developer paths in README.md; all routing logic is well-tested and the architectural change is sound.
  • The routing logic is thorough and backed by a comprehensive integration test suite covering every routing branch (rootless startup, single-project auto-select, explicit selector, file-path resolution, monorepo relative paths, ambiguity errors, workspace resource overview). The project-state module is cleanly separated with proper test isolation via clearProjects(). The one identified issue—hardcoded personal paths in README.md—is a docs-only defect that doesn't affect runtime behavior and is straightforward to fix.
  • README.md — contains hardcoded developer-specific paths in the "Test It Yourself" code examples (lines 268 and 281) that must be replaced with generic placeholders.

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]
Loading

Last reviewed commit: 4ee1832

README.md Outdated
"mcpServers": {
"codebase-context": {
"command": "node",
"args": ["C:/Users/bitaz/Repos/codebase-context/dist/index.js"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
"args": ["C:/Users/bitaz/Repos/codebase-context/dist/index.js"]
"args": ["<path-to-local-build>/dist/index.js"]

And in the second block:

Suggested change
"args": ["C:/Users/bitaz/Repos/codebase-context/dist/index.js"]
"args": ["<path-to-local-build>/dist/index.js", "/path/to/your/project"]

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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
Comment on lines +1226 to +1230
await Promise.all(
getKnownRootPaths().map((rootPath) =>
initProject(rootPath, watcherDebounceMs, { enableWatcher: false }).catch(() => {
/* best-effort prewarm */
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Comment on lines +1211 to +1215
.filter((root) => root.uri.startsWith('file://'))
.map((root) => ({
rootPath: fileURLToPath(root.uri),
label: root.label
}));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@PatrickSys
Copy link
Owner Author

Follow-up pushed in 27f1700.

Addressed from review:

  • removed the leaked local machine path from the README examples
  • validate file:// client roots before registering them
  • stopped eager background indexing of every announced root during root refresh
  • added regression coverage for both runtime fixes

Verification:

  • pnpm test -- tests/multi-project-routing.test.ts tests/project-discovery.test.ts tests/tools/dispatch.test.ts tests/cli.test.ts tests/resource-uri.test.ts
  • confirmed there are no remaining C:/Users/bitaz/... paths in README/docs/src/tests

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.

Single MCP server instance should work across multiple projects

1 participant