Skip to content

Commit 6298016

Browse files
committed
docs: clarify roots-first multi-project contract
1 parent a85f5e3 commit 6298016

5 files changed

Lines changed: 79 additions & 4 deletions

File tree

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Behavior matrix:
105105
Rules:
106106

107107
- 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.
108109
- If the server still cannot tell which project to use, a bootstrap path or explicit absolute `project` path remains the fallback.
109110
- `project` is the canonical explicit selector when routing is ambiguous.
110111
- `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

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)