Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -104,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.
Expand Down
16 changes: 14 additions & 2 deletions docs/client-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down
2 changes: 2 additions & 0 deletions scripts/run-vitest.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
6 changes: 2 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down
9 changes: 8 additions & 1 deletion src/utils/language-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ function buildCodeExtensions(extraExtensions?: Iterable<string>): Set<string> {
return merged;
}

// Cached default set — built once at module load, reused by callers that pass no extra extensions.
const defaultCodeExtensions: ReadonlySet<string> = buildCodeExtensions();

/**
* Detect language from file path
*/
Expand All @@ -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);
}

Expand Down
23 changes: 23 additions & 0 deletions tests/mcp-client-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
39 changes: 39 additions & 0 deletions tests/multi-project-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Loading