From a85f5e3aae111d628bc66a99ba381209fd2a9d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Rossell=C3=B3=20Colom?= <74001504+PatrickSys@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:30:31 +0100 Subject: [PATCH 1/3] docs: restructure README and add client setup guide (#82) * docs: restructure README and add client setup guide README cut from 707 to ~220 lines. Per-client config blocks, pipeline internals, and eval harness moved to dedicated docs where they belong. Screenshots and CLI previews moved up before the setup details. - docs/client-setup.md: new file with all 7 client configs (stdio + HTTP where supported), fallback single-project setup, and local build testing - docs/capabilities.md: add practical routing examples and selection_required response shape (moved from README) - CONTRIBUTING.md: add eval harness section (moved from README) - templates/mcp/stdio/.mcp.json + http/.mcp.json: copy-pasteable config templates - tests/mcp-client-templates.test.ts: regression tests for template validity and README/capabilities doc coverage - package.json: add docs/client-setup.md to files array * Update tests/mcp-client-templates.test.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- docs/capabilities.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/capabilities.md b/docs/capabilities.md index 60796fb..a2cc0c3 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -21,6 +21,7 @@ Per-project config overrides supported today: - `projects[].analyzerHints.analyzer`: prefers a registered analyzer by name for that project and falls back safely when the name is missing or invalid - `projects[].analyzerHints.extensions`: adds project-local source extensions for indexing and auto-refresh watching without changing defaults for other projects + Copy-pasteable client config templates are shipped in the package: - `templates/mcp/stdio/.mcp.json` — stdio setup for `.mcp.json`-style clients From 629801673bee5341d552a8f1dc3515c5a32265da Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Sat, 28 Mar 2026 17:40:27 +0100 Subject: [PATCH 2/3] docs: clarify roots-first multi-project contract --- CHANGELOG.md | 4 +-- docs/capabilities.md | 1 + docs/client-setup.md | 16 ++++++++++-- tests/mcp-client-templates.test.ts | 23 +++++++++++++++++ tests/multi-project-routing.test.ts | 39 +++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1abb7a2..ecc2cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ ### Documentation -- simplify the setup story around three cases: default rootless setup, single-project fallback, and explicit `project` retries -- clarify that issue #63 fixed the architecture and workspace-aware workflow, but issue #2 is not fully solved when the client does not provide enough project context +- simplify the setup story around a roots-first contract: roots-capable multi-project sessions, single-project fallback, and explicit `project` retries +- clarify that issue #63 fixed the architecture and workspace-aware workflow, but issue #2 is still only partially solved when the client does not provide roots or active-project context - remove the repo-local `init` / marker-file story from the public setup guidance ## [1.9.0](https://github.com/PatrickSys/codebase-context/compare/v1.8.2...v1.9.0) (2026-03-19) diff --git a/docs/capabilities.md b/docs/capabilities.md index a2cc0c3..547472a 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -105,6 +105,7 @@ Behavior matrix: Rules: - If the client provides workspace context, that becomes the trusted workspace boundary for the session. In practice this usually comes from MCP roots. +- Treat seamless multi-project routing as evidence-backed only for roots-capable hosts. Without roots, explicit fallback is still required. - If the server still cannot tell which project to use, a bootstrap path or explicit absolute `project` path remains the fallback. - `project` is the canonical explicit selector when routing is ambiguous. - `project` may point at a project path, file path, `file://` URI, or relative subproject path. diff --git a/docs/client-setup.md b/docs/client-setup.md index 4acdb02..23304e6 100644 --- a/docs/client-setup.md +++ b/docs/client-setup.md @@ -18,6 +18,18 @@ npx -y codebase-context --http --port 4000 Copy-pasteable templates: [`templates/mcp/stdio/.mcp.json`](../templates/mcp/stdio/.mcp.json) and [`templates/mcp/http/.mcp.json`](../templates/mcp/http/.mcp.json). +## Project routing contract + +Automatic multi-project routing is evidence-backed only when the MCP host announces workspace roots. Treat that as the primary path. + +If the host does not send roots, or still cannot tell which project is active, use one of the explicit fallbacks instead: + +- start the server with a single bootstrap path +- set `CODEBASE_ROOT` +- retry tool calls with `project` + +If multiple projects are available and no active project can be inferred safely, the server returns `selection_required` instead of guessing. + ## Claude Code ```bash @@ -197,9 +209,9 @@ Check these three flows: 1. **Single project** — call `search_codebase` or `metadata`. Routing is automatic. -2. **Multiple projects, one server entry** — open two repos or a monorepo. Call `codebase://context`. Expected: workspace overview, then automatic routing once a project is active. +2. **Multiple projects on a roots-capable host** — open two repos or a monorepo. Call `codebase://context`. Expected: workspace overview, then automatic routing once a project is active. -3. **Ambiguous selection** — start without a bootstrap path, call `search_codebase`. Expected: `selection_required`. Retry with `project` set to `apps/dashboard` or `/repos/customer-portal`. +3. **Ambiguous or no-roots selection** — start without a bootstrap path, call `search_codebase`. Expected: `selection_required`. Retry with `project` set to `apps/dashboard` or `/repos/customer-portal`. For monorepos, test all three selector forms: diff --git a/tests/mcp-client-templates.test.ts b/tests/mcp-client-templates.test.ts index 1d40405..03cf820 100644 --- a/tests/mcp-client-templates.test.ts +++ b/tests/mcp-client-templates.test.ts @@ -133,4 +133,27 @@ describe('docs/capabilities.md transport documentation', () => { expect(caps).toContain('Codex'); expect(caps).toContain('Windsurf'); }); + + it('states the roots-first routing fallback explicitly', () => { + expect(caps).toContain('roots-capable hosts'); + expect(caps).toContain('explicit fallback is still required'); + }); +}); + +describe('docs/client-setup.md multi-project guidance', () => { + const clientSetup = readText('docs/client-setup.md'); + + it('documents the project routing contract', () => { + expect(clientSetup).toContain( + 'Automatic multi-project routing is evidence-backed only when the MCP host announces workspace roots.' + ); + expect(clientSetup).toContain( + 'the server returns `selection_required` instead of guessing' + ); + }); + + it('keeps the three verification flows aligned with the roots-first contract', () => { + expect(clientSetup).toContain('Multiple projects on a roots-capable host'); + expect(clientSetup).toContain('Ambiguous or no-roots selection'); + }); }); diff --git a/tests/multi-project-routing.test.ts b/tests/multi-project-routing.test.ts index e091be9..3ee1d68 100644 --- a/tests/multi-project-routing.test.ts +++ b/tests/multi-project-routing.test.ts @@ -362,6 +362,45 @@ describe('multi-project routing', () => { } }); + it('triggers a background rebuild for a corrupted explicit project without falling back to cwd', async () => { + delete process.env.CODEBASE_ROOT; + delete process.argv[2]; + + await fs.rm(path.join(secondaryRoot, CODEBASE_CONTEXT_DIRNAME, INDEX_META_FILENAME), { + force: true + }); + + const { server, refreshKnownRootsFromClient } = await import('../src/index.js'); + const typedServer = server as unknown as TestServer & { + listRoots: () => Promise<{ roots: Array<{ uri: string; name?: string }> }>; + }; + const originalListRoots = typedServer.listRoots.bind(typedServer); + const handler = typedServer._requestHandlers.get('tools/call'); + if (!handler) throw new Error('tools/call handler not registered'); + + typedServer.listRoots = vi.fn().mockRejectedValue(new Error('roots unsupported')); + + try { + await refreshKnownRootsFromClient(); + const response = await callTool(handler, 21, 'search_codebase', { + query: 'feature', + project: secondaryRoot + }); + const payload = parsePayload(response) as { + status: string; + message: string; + index?: { action?: string; reason?: string }; + }; + + expect(payload.status).toBe('indexing'); + expect(payload.message).toContain('retry shortly'); + expect(payload.index?.action).toBe('rebuild-started'); + expect(String(payload.index?.reason || '')).toContain('Index meta'); + } finally { + typedServer.listRoots = originalListRoots; + } + }); + it('returns selection_required instead of silently falling back to cwd when startup is rootless and unresolved', async () => { delete process.env.CODEBASE_ROOT; delete process.argv[2]; From 41c252a644baf230ee0d1efb34387fad9d85e8eb Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Mon, 30 Mar 2026 11:02:01 +0200 Subject: [PATCH 3/3] fix: address greptile P2 review comments - Cache defaultCodeExtensions at module level in isCodeFile so callers that pass no pre-built set avoid a per-call Set allocation - Add motivation comment to scripts/run-vitest.mjs explaining why the wrapper exists instead of invoking vitest directly - Always assign runtimeOverrides in applyServerConfig even when empty, so stale overrides don't persist if config hints are later removed --- scripts/run-vitest.mjs | 2 ++ src/index.ts | 6 ++---- src/utils/language-detection.ts | 9 ++++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/run-vitest.mjs b/scripts/run-vitest.mjs index 32e164e..c529a5c 100644 --- a/scripts/run-vitest.mjs +++ b/scripts/run-vitest.mjs @@ -1,3 +1,5 @@ +// Spawns vitest via process.execPath to avoid bin-resolution failures when +// Node is invoked directly (e.g. `node scripts/run-vitest.mjs`) without pnpm. import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; diff --git a/src/index.ts b/src/index.ts index a520ed2..5f1005c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1667,10 +1667,8 @@ async function applyServerConfig( configRoots.set(rootKey, { rootPath: proj.root }); registerKnownRoot(proj.root); const runtimeOverrides = buildProjectRuntimeOverrides(proj); - if (Object.keys(runtimeOverrides).length > 0) { - const project = getOrCreateProject(proj.root); - project.runtimeOverrides = runtimeOverrides; - } + const project = getOrCreateProject(proj.root); + project.runtimeOverrides = runtimeOverrides; } catch { console.error(`[config] Skipping inaccessible project root: ${proj.root}`); } diff --git a/src/utils/language-detection.ts b/src/utils/language-detection.ts index 7999d84..2cc8bac 100644 --- a/src/utils/language-detection.ts +++ b/src/utils/language-detection.ts @@ -176,6 +176,9 @@ function buildCodeExtensions(extraExtensions?: Iterable): Set { return merged; } +// Cached default set — built once at module load, reused by callers that pass no extra extensions. +const defaultCodeExtensions: ReadonlySet = buildCodeExtensions(); + /** * Detect language from file path */ @@ -193,7 +196,11 @@ export function isCodeFile( ): boolean { const ext = path.extname(filePath).toLowerCase(); const supportedExtensions = - extensions instanceof Set ? extensions : buildCodeExtensions(extensions); + extensions instanceof Set + ? extensions + : extensions + ? buildCodeExtensions(extensions) + : defaultCodeExtensions; return supportedExtensions.has(ext); }