From ca04708827244d05677d8e318a0b346b7dde6a5b Mon Sep 17 00:00:00 2001 From: Chris Jones <190028784+LifeInTheWeeds@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:28:45 -0400 Subject: [PATCH] fix(mcp): return -32601 for unknown methods, and implement `ping` The MCP stdio server had a single error path, so every failure -- including "this method is not implemented" -- was reported as JSON-RPC -32603: _ => Err(anyhow!("Unknown method: {}", request.method)), ... code: -32603, // Internal error -32603 means the server itself failed. -32601 ("Method not found") means the method is simply not implemented. Answering a capability probe with "I have failed internally" is a materially different signal, and a strict client may reasonably treat the server as unhealthy. Three concrete effects, all reproduced against the stdio server: ping -> -32603 `ping` is REQUIRED by the MCP specification and is used for liveness checks. A client that pings during a long session gets an internal error and the correct response to that is to drop the connection. resources/list -> -32603 Clients probe these after initialize even when prompts/list -> -32603 the capability is not advertised. After this change: ping -> {"result":{}} resources/list -> {"result":{"resources":[]}} prompts/list -> {"result":{"prompts":[]}} totally/bogus -> {"error":{"code":-32601,...}} Scope note: this is independent of the DuckDB CLI work on `fix/duckdb-cli-engine-gate` and is branched from main so it can be reviewed and merged on its own. It does NOT fix any connection failure -- it was found while investigating one whose cause turned out to be unrelated. Build note: main does not currently compile against duckdb >= 1.10505.0, so this branch needs the `non_exhaustive` wildcard fix (first commit of fix/duckdb-cli-engine-gate) to build. The behaviour above was verified with that fix applied locally. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016pWGjWtzGf4UvtgiWqYQyR --- src/mcp.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/mcp.rs b/src/mcp.rs index 449395d..85c6fce 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -196,11 +196,24 @@ pub async fn serve() -> Result<()> { /// /// Routes the request to the appropriate handler based on the method name. async fn handle_request(request: JsonRpcRequest) -> JsonRpcResponse { - let result = match request.method.as_str() { - "initialize" => handle_initialize(request.params), - "tools/list" => handle_list_tools(), - "tools/call" => handle_call_tool(request.params).await, - _ => Err(anyhow!("Unknown method: {}", request.method)), + // JSON-RPC 2.0 reserves -32601 for an unimplemented method; -32603 ("Internal + // error") means the server itself failed. Reporting -32603 for a method we + // simply do not implement tells the client the server is broken, and strict + // clients drop the connection on it during capability probing. + const METHOD_NOT_FOUND: i32 = -32601; + const INTERNAL_ERROR: i32 = -32603; + + let (result, err_code) = match request.method.as_str() { + "initialize" => (handle_initialize(request.params), INTERNAL_ERROR), + "tools/list" => (handle_list_tools(), INTERNAL_ERROR), + "tools/call" => (handle_call_tool(request.params).await, INTERNAL_ERROR), + // `ping` is REQUIRED by the MCP spec; an empty result is its success shape. + "ping" => (Ok(serde_json::json!({})), INTERNAL_ERROR), + // Clients probe these after initialize even when the capability is not + // advertised. Answer empty rather than erroring. + "resources/list" => (Ok(serde_json::json!({ "resources": [] })), INTERNAL_ERROR), + "prompts/list" => (Ok(serde_json::json!({ "prompts": [] })), INTERNAL_ERROR), + _ => (Err(anyhow!("Unknown method: {}", request.method)), METHOD_NOT_FOUND), }; match result { @@ -214,11 +227,7 @@ async fn handle_request(request: JsonRpcRequest) -> JsonRpcResponse { jsonrpc: "2.0".to_string(), id: request.id, result: None, - error: Some(JsonRpcError { - code: -32603, // Internal error - message: e.to_string(), - data: None, - }), + error: Some(JsonRpcError { code: err_code, message: e.to_string(), data: None }), }, } }