feat(mcp): unindexed sessions go quiet — empty tools/list + inactive instructions, no-error policy (#769) (#817)
An MCP session in a workspace with no .codegraph/ previously got the full "lean on codegraph for everything" playbook plus all 8 tools, then every call returned isError — and one or two early errors teach an agent to abandon codegraph for the whole session (maintainer-observed). Now the initialize response picks an instructions variant by index state (cheap sync walk-up, #172 respond-fast contract holds) and tools/list serves an EMPTY list when unindexed: absence is the one signal an agent can't misread. Indexing is deliberately the user's call — the inactive note tells the agent not to run init itself. No-error policy in the tool handler: expected/recoverable conditions (NotIndexedError — cross-project query to an unindexed path, default- project detection miss) return SUCCESS-shaped guidance instead of isError; security refusals (PathRefusalError) stay hard errors without retry encouragement; genuine internal failures keep isError but add a retry-once note so a transient blip doesn't convert to permanent abandonment. Principle recorded in CLAUDE.md. Also: codegraph_search kind:"type" (advertised by its own schema enum) silently matched nothing — now maps to type_alias; codegraph_explore's query param no longer tells agents to run codegraph_search first (contradicted explore's call-FIRST design); server-instructions §Limitations rewords the unindexed case to stay-out-for-the-session. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0682681175
commit
f9fcc2cd6a
@@ -71,8 +71,29 @@ typically one to a few calls; a grep/read exploration is dozens.
|
||||
|
||||
## Limitations
|
||||
|
||||
- If a tool reports the project isn't initialized, \`.codegraph/\` doesn't exist yet — offer to run \`codegraph init -i\` to build the index.
|
||||
- If a tool reports a project isn't indexed (no \`.codegraph/\`), stop calling codegraph tools for that project for the rest of the session and use your built-in tools there instead. Indexing is the user's decision — mention they can run \`codegraph init\` if it comes up, but don't run it yourself.
|
||||
- Index lags file writes by ~1 second.
|
||||
- Cross-file resolution is best-effort name matching; ambiguous calls may return multiple candidates.
|
||||
- No live correctness validation — that's still the TypeScript compiler / test suite / linter's job. Codegraph supplements those with structural context they don't have.
|
||||
`;
|
||||
|
||||
/**
|
||||
* Instructions variant sent when the workspace has NO codegraph index.
|
||||
*
|
||||
* Sending the full playbook ("lean on codegraph for everything") into a
|
||||
* session where every call would fail wastes the agent's calls and — worse —
|
||||
* the failures teach it codegraph is broken. The unindexed variant is a
|
||||
* short, unambiguous "inactive this session" note; `tools/list` is gated to
|
||||
* empty in the same state, so the agent has nothing to mis-call. Indexing is
|
||||
* deliberately left to the user: the agent is told NOT to run init itself.
|
||||
*/
|
||||
export const SERVER_INSTRUCTIONS_UNINDEXED = `# Codegraph — inactive (workspace not indexed)
|
||||
|
||||
This workspace has no codegraph index (no \`.codegraph/\` directory), so no
|
||||
codegraph tools are available this session. Work with your built-in tools as
|
||||
usual.
|
||||
|
||||
Indexing is the user's decision — do not run it yourself. If the user asks
|
||||
about codegraph, they can enable it by running \`codegraph init\` in the
|
||||
project root and starting a new session.
|
||||
`;
|
||||
|
||||
+23
-3
@@ -16,8 +16,9 @@ import * as path from 'path';
|
||||
import { JsonRpcRequest, JsonRpcNotification, JsonRpcTransport, ErrorCodes } from './transport';
|
||||
import { MCPEngine } from './engine';
|
||||
import { tools } from './tools';
|
||||
import { SERVER_INSTRUCTIONS } from './server-instructions';
|
||||
import { SERVER_INSTRUCTIONS, SERVER_INSTRUCTIONS_UNINDEXED } from './server-instructions';
|
||||
import { CodeGraphPackageVersion } from './version';
|
||||
import { findNearestCodeGraphRoot } from '../directory';
|
||||
|
||||
/**
|
||||
* MCP Server Info — kept on the session because some clients log it. The
|
||||
@@ -178,12 +179,24 @@ export class MCPSession {
|
||||
explicitPath = this.explicitProjectPath;
|
||||
}
|
||||
|
||||
// Pick the instructions variant by the workspace'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.
|
||||
const indexed = findNearestCodeGraphRoot(explicitPath ?? process.cwd()) !== null;
|
||||
|
||||
// Respond to the handshake BEFORE doing any heavy init — see issue #172.
|
||||
this.transport.sendResult(request.id, {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: SERVER_INFO,
|
||||
instructions: SERVER_INSTRUCTIONS,
|
||||
instructions: indexed ? SERVER_INSTRUCTIONS : SERVER_INSTRUCTIONS_UNINDEXED,
|
||||
});
|
||||
|
||||
if (explicitPath) {
|
||||
@@ -196,8 +209,15 @@ 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.
|
||||
this.transport.sendResult(request.id, {
|
||||
tools: this.engine.getToolHandler().getTools(),
|
||||
tools: this.engine.hasDefaultCodeGraph() ? this.engine.getToolHandler().getTools() : [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+52
-8
@@ -28,6 +28,25 @@ import {
|
||||
} from 'fs';
|
||||
import { clamp, validatePathWithinRoot, validateProjectPath, isConfigLeafNode, CONFIG_LEAF_LANGUAGES } from '../utils';
|
||||
import { isGeneratedFile } from '../extraction/generated-detection';
|
||||
|
||||
/**
|
||||
* An expected, recoverable "codegraph can't serve this" condition — most
|
||||
* importantly a project with no index. The dispatch catch converts these to
|
||||
* SUCCESS-shaped responses (guidance text, NO isError): an `isError: true`
|
||||
* early in a session teaches the agent the toolset is broken and it stops
|
||||
* calling codegraph entirely (observed repeatedly), which is exactly wrong
|
||||
* for conditions the agent can simply work around (use built-in tools for
|
||||
* that codebase / pass projectPath). isError is reserved for "stop trying"
|
||||
* cases: security refusals ({@link PathRefusalError}) and genuine
|
||||
* malfunctions.
|
||||
*/
|
||||
export class NotIndexedError extends Error {}
|
||||
|
||||
/**
|
||||
* A security refusal (sensitive system path). Stays `isError: true` WITHOUT
|
||||
* retry guidance — abandoning this path is the desired agent reaction.
|
||||
*/
|
||||
export class PathRefusalError extends Error {}
|
||||
import { resolve as resolvePath } from 'path';
|
||||
|
||||
/** Maximum output length to prevent context bloat (characters) */
|
||||
@@ -522,7 +541,7 @@ export const tools: ToolDefinition[] = [
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts"). Use codegraph_search first to find relevant names.',
|
||||
description: 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts"). For a flow question, name the symbols spanning the flow (e.g. "mutateElement renderScene"). A natural-language question works too — no prior codegraph_search needed.',
|
||||
},
|
||||
maxFiles: {
|
||||
type: 'number',
|
||||
@@ -752,14 +771,16 @@ export class ToolHandler {
|
||||
if (!projectPath) {
|
||||
if (!this.cg) {
|
||||
const searched = this.defaultProjectHint ?? process.cwd();
|
||||
throw new Error(
|
||||
throw new NotIndexedError(
|
||||
'No CodeGraph project is loaded for this session.\n' +
|
||||
`Searched for a .codegraph/ directory starting from: ${searched}\n` +
|
||||
'The index is likely fine — this is a working-directory detection issue: ' +
|
||||
'If this project IS indexed, this is a working-directory detection issue: ' +
|
||||
"the MCP client launched the server outside your project and didn't report the " +
|
||||
'workspace root. Fix it either way:\n' +
|
||||
' • Pass projectPath to the tool call, e.g. projectPath: "/absolute/path/to/your/project"\n' +
|
||||
' • Or add --path to the server\'s MCP config args: ["serve", "--mcp", "--path", "/absolute/path/to/your/project"]'
|
||||
' • Or add --path to the server\'s MCP config args: ["serve", "--mcp", "--path", "/absolute/path/to/your/project"]\n' +
|
||||
'If the project simply has no index, continue with your built-in tools (Read/Grep/Glob) ' +
|
||||
"and don't call codegraph again this session — the user can run 'codegraph init' to enable it."
|
||||
);
|
||||
}
|
||||
return this.cg;
|
||||
@@ -778,7 +799,7 @@ export class ToolHandler {
|
||||
if (existsSync(projectPath)) {
|
||||
const pathError = validateProjectPath(projectPath);
|
||||
if (pathError) {
|
||||
throw new Error(pathError);
|
||||
throw new PathRefusalError(pathError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,7 +807,12 @@ export class ToolHandler {
|
||||
const resolvedRoot = findNearestCodeGraphRoot(projectPath);
|
||||
|
||||
if (!resolvedRoot) {
|
||||
throw new Error(`CodeGraph not initialized in ${projectPath}. Run 'codegraph init' in that project first.`);
|
||||
throw new NotIndexedError(
|
||||
`The project at ${projectPath} isn't indexed with codegraph (no .codegraph/ directory found ` +
|
||||
'walking up from it), so codegraph cannot query it. Use your built-in tools (Read/Grep/Glob) ' +
|
||||
"for that codebase instead, and don't call codegraph for it again this session. " +
|
||||
"Indexing is the user's decision — they can run 'codegraph init' in that project to enable it."
|
||||
);
|
||||
}
|
||||
|
||||
// If the path resolves to the default project, reuse the already-open
|
||||
@@ -1069,7 +1095,21 @@ export class ToolHandler {
|
||||
const withWorktree = this.withWorktreeNotice(result, args.projectPath as string | undefined);
|
||||
return this.withStalenessNotice(withWorktree, args.projectPath as string | undefined);
|
||||
} catch (err) {
|
||||
return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
// Expected condition, not a malfunction: answer as a SUCCESS so the
|
||||
// agent keeps trusting the toolset for projects that ARE indexed.
|
||||
// (An isError here teaches session-long abandonment — see NotIndexedError.)
|
||||
if (err instanceof NotIndexedError) {
|
||||
return this.textResult(err.message);
|
||||
}
|
||||
// Security refusal: a clean error, no retry encouragement.
|
||||
if (err instanceof PathRefusalError) {
|
||||
return this.errorResult(err.message);
|
||||
}
|
||||
return this.errorResult(
|
||||
`Tool execution failed: ${err instanceof Error ? err.message : String(err)}. ` +
|
||||
'This is an internal codegraph error — retry the call once; if it persists, ' +
|
||||
'continue without codegraph for this task.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1081,7 +1121,11 @@ export class ToolHandler {
|
||||
if (typeof query !== 'string') return query;
|
||||
|
||||
const cg = this.getCodeGraph(args.projectPath as string | undefined);
|
||||
const kind = args.kind as string | undefined;
|
||||
const rawKind = args.kind as string | undefined;
|
||||
// The schema enum says 'type' (what agents naturally reach for); the
|
||||
// NodeKind is 'type_alias'. Without the mapping, kind: "type" silently
|
||||
// matched nothing — a filter value we advertise must work.
|
||||
const kind = rawKind === 'type' ? 'type_alias' : rawKind;
|
||||
const rawLimit = Number(args.limit) || 10;
|
||||
const limit = clamp(rawLimit, 1, 100);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user