fix(mcp): serve tools without a root index + make the front-load hook monorepo-aware (#964) (#966)

The MCP server gated tool availability on whether the server root had a
.codegraph/ index, so in a monorepo where only sub-projects are indexed the
agent saw zero tools — and couldn't reach an indexed sub-project even by
projectPath. A session started before `codegraph init` also never surfaced the
tools afterward. The Claude front-load hook had the mirror gap: it only walked
UP for an index, so it stayed silent at a monorepo root.

MCP server:
- Always expose the tool surface; when the root isn't indexed, send a
  per-project instructions variant (pass projectPath) instead of the
  "inactive" note. Safety comes from response SHAPE (success-shaped guidance,
  never isError), not from hiding tools.
- Reword the no-default-project guidance to be per-project, not per-session,
  and sharpen the projectPath schema description.

Front-load hook (UserPromptSubmit):
- Scan DOWN (bounded depth, workspace-root-gated) for indexed sub-projects and
  shape the injection by topology: front-load the one the prompt names, nudge
  about the rest, or list them when ambiguous.

Verified: full suite (1703 passed); a live two-package monorepo run confirms the
hook front-loads the correct sub-project with no cross-package leakage. The
front-load's net speed effect is the existing multi-file-vs-single-file
tradeoff, unchanged by this work.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-23 12:57:47 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 0a91d0f512
commit 85a8f32fd9
10 changed files with 431 additions and 87 deletions
+24 -19
View File
@@ -16,7 +16,7 @@ import * as path from 'path';
import { JsonRpcRequest, JsonRpcNotification, JsonRpcTransport, ErrorCodes } from './transport';
import { MCPEngine } from './engine';
import { tools } from './tools';
import { SERVER_INSTRUCTIONS, SERVER_INSTRUCTIONS_UNINDEXED } from './server-instructions';
import { SERVER_INSTRUCTIONS, SERVER_INSTRUCTIONS_NO_ROOT_INDEX } from './server-instructions';
import { CodeGraphPackageVersion } from './version';
import { findNearestCodeGraphRoot } from '../directory';
import { getTelemetry, ClientInfo } from '../telemetry';
@@ -189,16 +189,17 @@ export class MCPSession {
explicitPath = this.explicitProjectPath;
}
// Pick the instructions variant by the workspace's index state — a cheap
// Pick the instructions variant by the root's index state — a cheap
// synchronous walk-up (existsSync loop only, no DB open, so the #172
// respond-fast contract holds). An unindexed workspace gets the short
// "inactive this session" note instead of the full playbook: the playbook
// tells the agent to lean on tools that would all fail, and early failures
// teach the agent to abandon codegraph entirely. `tools/list` is gated the
// same way (empty list when unindexed). When no explicit path is known yet
// (roots/list dance pending), cwd is the best predictor of where the
// default project will resolve — and on a mismatch the worst case is the
// optimistic full playbook backstopped by the empty tool list.
// respond-fast contract holds). When the root IS indexed, send the full
// single-project playbook. When it ISN'T, send the per-project variant
// (tools are still exposed — see handleToolsList): it tells the agent there
// is no default project and to pass `projectPath` to any project that has a
// `.codegraph/`. Gating tool AVAILABILITY on whether `./` is indexed was the
// #964 bug — it broke monorepos (only sub-projects indexed) and never
// surfaced the tools after a mid-session `codegraph init`. When no explicit
// path is known yet (roots/list dance pending), cwd is the best predictor of
// where the default project will resolve.
const indexed = findNearestCodeGraphRoot(explicitPath ?? process.cwd()) !== null;
// Respond to the handshake BEFORE doing any heavy init — see issue #172.
@@ -206,7 +207,7 @@ export class MCPSession {
protocolVersion: PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: SERVER_INFO,
instructions: indexed ? SERVER_INSTRUCTIONS : SERVER_INSTRUCTIONS_UNINDEXED,
instructions: indexed ? SERVER_INSTRUCTIONS : SERVER_INSTRUCTIONS_NO_ROOT_INDEX,
});
if (explicitPath) {
@@ -219,15 +220,19 @@ export class MCPSession {
private async handleToolsList(request: JsonRpcRequest): Promise<void> {
await this.retryInitIfNeeded();
// An unindexed workspace serves an EMPTY tool list: absence is the one
// signal an agent can't misread. Listing 8 tools that all fail wastes the
// agent's calls and teaches it codegraph is broken (observed: one or two
// early isError responses and the agent stops calling codegraph for the
// whole session). A `codegraph init` run after the server started is
// picked up on the next tools/list — retryInitIfNeeded re-walks — though
// most hosts only request the list once per connection.
// Always expose the tools — even when the server root has no index. Gating
// availability on whether `./` is indexed (the old behavior) breaks the
// monorepo case where only sub-projects carry a `.codegraph/` (the agent
// saw zero tools and couldn't even reach an indexed sub-project by
// `projectPath`), and it hides the tools from a session that started before
// the user ran `codegraph init` (most hosts request the list once, so the
// freshly-built index never surfaces). #964. The not-indexed case is still
// safe: a call against an un-indexed path returns SUCCESS-shaped guidance
// ("pass projectPath / run codegraph init"), never `isError`, so it can't
// teach the agent to abandon codegraph. `getTools()` returns the default
// surface even before a project is open.
this.transport.sendResult(request.id, {
tools: this.engine.hasDefaultCodeGraph() ? this.engine.getToolHandler().getTools() : [],
tools: this.engine.getToolHandler().getTools(),
});
}