From fb6153d3699418012a48aadcc4e0388fde8fc641 Mon Sep 17 00:00:00 2001 From: vedantlavale Date: Fri, 21 Aug 2026 10:44:42 +0530 Subject: [PATCH] security(mcp): fix stdio dispatcher type confusion (Z-052) Only messages without a method are responses. Server-initiated requests (method + id) were previously treated as responses when the id collided with a pending client request, causing silent empty results. Update readLoop to validate method before dispatch, route server requests to a handler that replies with method not found (-32601) and never misdelivers to pending callers. Add diagnostics logging for unexpected message types. Mirror the method check in remoteSSEClient.deliverEventMessage for SSE transports. Fixes #924 --- internal/mcp/client.go | 27 +++++++++++++++++++++++++++ internal/mcp/network_client.go | 6 ++++++ 2 files changed, 33 insertions(+) diff --git a/internal/mcp/client.go b/internal/mcp/client.go index 064e7f213..b14760e31 100644 --- a/internal/mcp/client.go +++ b/internal/mcp/client.go @@ -385,7 +385,34 @@ func (client *Client) readLoop() { client.failAll(err) return } + // Explicit type validation: distinguish requests/notifications (have + // method) from responses (no method, has id). Only responses may be + // delivered to pending callers. Server-initiated requests must never be + // misdelivered as a response even when the id collides with a pending + // client request (Z-052). + if message.Method != "" { + if message.ID != nil { + // Server-initiated request: route to request handler and + // reply with method-not-found so the server does not hang. + // The client does not currently handle inbound requests, so + // every method is unknown. Log for diagnostics. + _, _ = fmt.Fprintf(os.Stderr, "[mcp] server request %q (id %v) not handled: replying method not found\n", message.Method, message.ID) + client.mu.Lock() + _ = client.writer.write(rpcMessage{ + JSONRPC: "2.0", + ID: message.ID, + Error: &rpcError{Code: -32601, Message: "method not found: " + message.Method}, + }) + client.mu.Unlock() + } + // Notifications (no id) and handled server requests are not + // responses — do not dispatch. + continue + } if message.ID == nil { + // No id and no method: invalid or stray message. Ignore but log + // for diagnostics to help trace protocol mismatches. + _, _ = fmt.Fprintf(os.Stderr, "[mcp] ignoring unexpected message without id (method %q)\n", message.Method) continue } id, ok := rpcMessageID(message.ID) diff --git a/internal/mcp/network_client.go b/internal/mcp/network_client.go index b422b3c28..33e93c6ef 100644 --- a/internal/mcp/network_client.go +++ b/internal/mcp/network_client.go @@ -539,6 +539,12 @@ func (client *remoteSSEClient) deliverEventMessage(value string) error { if err := decoder.Decode(&message); err != nil { return fmt.Errorf("decode MCP SSE stream message: %w", err) } + // Explicit type validation: only messages without a method are responses. + // Server-initiated requests (method set) must not be misdelivered to a + // pending caller even when the id collides (Z-052). + if message.Method != "" { + return nil + } key := rpcResponseKey(message.ID) if key == "" { return nil