Skip to content

Commit 7cc643e

Browse files
docs: clarify roots-first multi-project contract (#85)
* 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: clarify roots-first multi-project contract * 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 --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent 4441b41 commit 7cc643e

File tree

8 files changed

+92
-9
lines changed

8 files changed

+92
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
### Documentation
1616

17-
- simplify the setup story around three cases: default rootless setup, single-project fallback, and explicit `project` retries
18-
- 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
17+
- simplify the setup story around a roots-first contract: roots-capable multi-project sessions, single-project fallback, and explicit `project` retries
18+
- 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
1919
- remove the repo-local `init` / marker-file story from the public setup guidance
2020

2121
## [1.9.0](https://github.com/PatrickSys/codebase-context/compare/v1.8.2...v1.9.0) (2026-03-19)

docs/capabilities.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Per-project config overrides supported today:
2121
- `projects[].analyzerHints.analyzer`: prefers a registered analyzer by name for that project and falls back safely when the name is missing or invalid
2222
- `projects[].analyzerHints.extensions`: adds project-local source extensions for indexing and auto-refresh watching without changing defaults for other projects
2323

24+
2425
Copy-pasteable client config templates are shipped in the package:
2526

2627
- `templates/mcp/stdio/.mcp.json` — stdio setup for `.mcp.json`-style clients
@@ -104,6 +105,7 @@ Behavior matrix:
104105
Rules:
105106

106107
- If the client provides workspace context, that becomes the trusted workspace boundary for the session. In practice this usually comes from MCP roots.
108+
- Treat seamless multi-project routing as evidence-backed only for roots-capable hosts. Without roots, explicit fallback is still required.
107109
- If the server still cannot tell which project to use, a bootstrap path or explicit absolute `project` path remains the fallback.
108110
- `project` is the canonical explicit selector when routing is ambiguous.
109111
- `project` may point at a project path, file path, `file://` URI, or relative subproject path.

docs/client-setup.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ npx -y codebase-context --http --port 4000
1818

1919
Copy-pasteable templates: [`templates/mcp/stdio/.mcp.json`](../templates/mcp/stdio/.mcp.json) and [`templates/mcp/http/.mcp.json`](../templates/mcp/http/.mcp.json).
2020

21+
## Project routing contract
22+
23+
Automatic multi-project routing is evidence-backed only when the MCP host announces workspace roots. Treat that as the primary path.
24+
25+
If the host does not send roots, or still cannot tell which project is active, use one of the explicit fallbacks instead:
26+
27+
- start the server with a single bootstrap path
28+
- set `CODEBASE_ROOT`
29+
- retry tool calls with `project`
30+
31+
If multiple projects are available and no active project can be inferred safely, the server returns `selection_required` instead of guessing.
32+
2133
## Claude Code
2234

2335
```bash
@@ -197,9 +209,9 @@ Check these three flows:
197209

198210
1. **Single project** — call `search_codebase` or `metadata`. Routing is automatic.
199211

200-
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.
212+
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.
201213

202-
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`.
214+
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`.
203215

204216
For monorepos, test all three selector forms:
205217

scripts/run-vitest.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Spawns vitest via process.execPath to avoid bin-resolution failures when
2+
// Node is invoked directly (e.g. `node scripts/run-vitest.mjs`) without pnpm.
13
import { spawn } from 'node:child_process';
24
import { fileURLToPath } from 'node:url';
35

src/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,10 +1667,8 @@ async function applyServerConfig(
16671667
configRoots.set(rootKey, { rootPath: proj.root });
16681668
registerKnownRoot(proj.root);
16691669
const runtimeOverrides = buildProjectRuntimeOverrides(proj);
1670-
if (Object.keys(runtimeOverrides).length > 0) {
1671-
const project = getOrCreateProject(proj.root);
1672-
project.runtimeOverrides = runtimeOverrides;
1673-
}
1670+
const project = getOrCreateProject(proj.root);
1671+
project.runtimeOverrides = runtimeOverrides;
16741672
} catch {
16751673
console.error(`[config] Skipping inaccessible project root: ${proj.root}`);
16761674
}

src/utils/language-detection.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ function buildCodeExtensions(extraExtensions?: Iterable<string>): Set<string> {
176176
return merged;
177177
}
178178

179+
// Cached default set — built once at module load, reused by callers that pass no extra extensions.
180+
const defaultCodeExtensions: ReadonlySet<string> = buildCodeExtensions();
181+
179182
/**
180183
* Detect language from file path
181184
*/
@@ -193,7 +196,11 @@ export function isCodeFile(
193196
): boolean {
194197
const ext = path.extname(filePath).toLowerCase();
195198
const supportedExtensions =
196-
extensions instanceof Set ? extensions : buildCodeExtensions(extensions);
199+
extensions instanceof Set
200+
? extensions
201+
: extensions
202+
? buildCodeExtensions(extensions)
203+
: defaultCodeExtensions;
197204
return supportedExtensions.has(ext);
198205
}
199206

tests/mcp-client-templates.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,27 @@ describe('docs/capabilities.md transport documentation', () => {
133133
expect(caps).toContain('Codex');
134134
expect(caps).toContain('Windsurf');
135135
});
136+
137+
it('states the roots-first routing fallback explicitly', () => {
138+
expect(caps).toContain('roots-capable hosts');
139+
expect(caps).toContain('explicit fallback is still required');
140+
});
141+
});
142+
143+
describe('docs/client-setup.md multi-project guidance', () => {
144+
const clientSetup = readText('docs/client-setup.md');
145+
146+
it('documents the project routing contract', () => {
147+
expect(clientSetup).toContain(
148+
'Automatic multi-project routing is evidence-backed only when the MCP host announces workspace roots.'
149+
);
150+
expect(clientSetup).toContain(
151+
'the server returns `selection_required` instead of guessing'
152+
);
153+
});
154+
155+
it('keeps the three verification flows aligned with the roots-first contract', () => {
156+
expect(clientSetup).toContain('Multiple projects on a roots-capable host');
157+
expect(clientSetup).toContain('Ambiguous or no-roots selection');
158+
});
136159
});

tests/multi-project-routing.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,45 @@ describe('multi-project routing', () => {
362362
}
363363
});
364364

365+
it('triggers a background rebuild for a corrupted explicit project without falling back to cwd', async () => {
366+
delete process.env.CODEBASE_ROOT;
367+
delete process.argv[2];
368+
369+
await fs.rm(path.join(secondaryRoot, CODEBASE_CONTEXT_DIRNAME, INDEX_META_FILENAME), {
370+
force: true
371+
});
372+
373+
const { server, refreshKnownRootsFromClient } = await import('../src/index.js');
374+
const typedServer = server as unknown as TestServer & {
375+
listRoots: () => Promise<{ roots: Array<{ uri: string; name?: string }> }>;
376+
};
377+
const originalListRoots = typedServer.listRoots.bind(typedServer);
378+
const handler = typedServer._requestHandlers.get('tools/call');
379+
if (!handler) throw new Error('tools/call handler not registered');
380+
381+
typedServer.listRoots = vi.fn().mockRejectedValue(new Error('roots unsupported'));
382+
383+
try {
384+
await refreshKnownRootsFromClient();
385+
const response = await callTool(handler, 21, 'search_codebase', {
386+
query: 'feature',
387+
project: secondaryRoot
388+
});
389+
const payload = parsePayload(response) as {
390+
status: string;
391+
message: string;
392+
index?: { action?: string; reason?: string };
393+
};
394+
395+
expect(payload.status).toBe('indexing');
396+
expect(payload.message).toContain('retry shortly');
397+
expect(payload.index?.action).toBe('rebuild-started');
398+
expect(String(payload.index?.reason || '')).toContain('Index meta');
399+
} finally {
400+
typedServer.listRoots = originalListRoots;
401+
}
402+
});
403+
365404
it('returns selection_required instead of silently falling back to cwd when startup is rootless and unresolved', async () => {
366405
delete process.env.CODEBASE_ROOT;
367406
delete process.argv[2];

0 commit comments

Comments
 (0)