mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 19:41:47 +08:00
fix(mcp): a tools/call whose arguments aren't an object gets invalid params, not a crash (#3071)
A tools/call request passes its arguments as an object. MCPSession took
params.arguments || {} without checking that, so a string or number went
straight to the tool handler, where the session-view check
('_cgExploreSession' in args) threw. The call then came back as a tool
result flagged isError: "This is an internal codegraph error, retry the
call once; if it persists, continue without codegraph". That reports a
client's malformed request as codegraph's own malfunction, with advice to
stop using it. An array went through as if it were the arguments. The
stdio proxy, serving in process while the daemon is unavailable, had the
same unchecked read.
Both now read the arguments through one helper, toolCallArguments(), and
answer anything but an object with a JSON-RPC invalid-params error
(-32602, "Tool arguments must be an object"), as a call with no tool
name already is; no tool runs, and the proxy starts no engine for it.
Missing, null or another empty value still means no arguments.
Validation: new mcp-tool-call-arguments.test.ts covers the session (the
direct stdio server and every daemon client) and the real proxy entry
point in a child process with the daemon unavailable: red on main (2 of
3 failed; the well-formed control passed), green with the fix.
explore-session-state and proxy-shutdown pass. Driving serve --mcp over
stdio: on main, codegraph_explore, codegraph_node and codegraph_search
with a string argument returned isError "internal codegraph error" and
codegraph_status accepted [1], 42 and true; with the fix those six get
-32602, while "", null, {} and absent arguments run as before.
Reimplements PR #2986 by @kjgggggg.
Fixes #2981
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
de0a4eb362
commit
fd57eaaa45
+8
-3
@@ -28,7 +28,7 @@ import { armStartupHandshakeTimeout } from './startup-handshake';
|
||||
import { treatStdinFailureAsShutdown } from './stdin-teardown';
|
||||
import { treatStdoutFailureAsShutdown } from './stdout-teardown';
|
||||
import { CodeGraphPackageVersion, isOlderRelease } from './version';
|
||||
import { SERVER_INFO, PROTOCOL_VERSION, initializeInstructions } from './session';
|
||||
import { SERVER_INFO, PROTOCOL_VERSION, initializeInstructions, toolCallArguments, TOOL_ARGUMENTS_NOT_OBJECT } from './session';
|
||||
import { SERVER_INSTRUCTIONS } from './server-instructions';
|
||||
import { getStaticTools } from './tools';
|
||||
import { ErrorCodes } from './transport';
|
||||
@@ -361,10 +361,15 @@ export async function runLocalHandshakeProxy(deps: LocalHandshakeDeps): Promise<
|
||||
let msg: JsonRpc; try { msg = JSON.parse(line) as JsonRpc; } catch { return; }
|
||||
const id = msg.id;
|
||||
if (msg.method === 'tools/call' && id !== undefined) {
|
||||
const params = (msg.params || {}) as { name: string; arguments?: unknown };
|
||||
const args = toolCallArguments(params);
|
||||
if (!args) {
|
||||
writeClient({ jsonrpc: '2.0', id, error: { code: ErrorCodes.InvalidParams, message: TOOL_ARGUMENTS_NOT_OBJECT } });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const local = await ensureEngine();
|
||||
const params = (msg.params || {}) as { name: string; arguments?: Record<string, unknown> };
|
||||
const result = await local.getToolHandler().execute(params.name, params.arguments || {}, exploreSession);
|
||||
const result = await local.getToolHandler().execute(params.name, args, exploreSession);
|
||||
writeClient({ jsonrpc: '2.0', id, result });
|
||||
getTelemetry().recordUsage('mcp_tool', params.name, !result.isError, telemetryClient);
|
||||
} catch (err) {
|
||||
|
||||
+22
-1
@@ -58,6 +58,23 @@ export function initializeInstructions(base: string, notice: string | null = get
|
||||
/** MCP Protocol Version (latest the server claims). */
|
||||
export const PROTOCOL_VERSION = '2024-11-05';
|
||||
|
||||
/** The invalid-params message for {@link toolCallArguments}' null. */
|
||||
export const TOOL_ARGUMENTS_NOT_OBJECT = 'Tool arguments must be an object';
|
||||
|
||||
/**
|
||||
* The arguments a `tools/call` passes, or null when they are not an object.
|
||||
* A string, number or array there is a malformed request, the client's to
|
||||
* fix, so it is answered with a JSON-RPC invalid-params error, as a call that
|
||||
* names no tool is, and never run: run, a string made the tool handler throw,
|
||||
* which came back as an internal codegraph error to retry (#2981). Exported so
|
||||
* the proxy, serving in process while the daemon is unavailable, answers it
|
||||
* the same way. No arguments (missing, null or another empty value) are none.
|
||||
*/
|
||||
export function toolCallArguments(params: { arguments?: unknown }): Record<string, unknown> | null {
|
||||
const args = params.arguments || {};
|
||||
return typeof args === 'object' && !Array.isArray(args) ? (args as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* How long to wait for the client's `roots/list` response before giving up
|
||||
* and falling back to the process cwd.
|
||||
@@ -296,7 +313,11 @@ export class MCPSession {
|
||||
}
|
||||
|
||||
const toolName = params.name;
|
||||
const toolArgs = params.arguments || {};
|
||||
const toolArgs = toolCallArguments(params);
|
||||
if (!toolArgs) {
|
||||
this.transport.sendError(request.id, ErrorCodes.InvalidParams, TOOL_ARGUMENTS_NOT_OBJECT);
|
||||
return;
|
||||
}
|
||||
|
||||
const tool = tools.find((t) => t.name === toolName);
|
||||
if (!tool) {
|
||||
|
||||
Reference in New Issue
Block a user