Skip to content

Unexpected tool exceptions leak internal details to clients via JSON-RPC error data #485

Description

@jj-ivx

Summary

When a tool handler raises an unexpected exception, the exception message is sent verbatim to the MCP client in the JSON-RPC error response's data member.

{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"Internal error","data":"Internal error calling tool list_entity_types: undefined local variable or method 'foo' for class Tools::ListEntityTypes"}}

Where (v1.0.0)

  • MCP::Server#call_tool wraps any unexpected exception into RequestHandlerError.new("Internal error calling tool #{tool_name}: #{e.message}", ...)
  • JsonRpcHandler.handle_request_error falls back to error.message for the data member, so the embedded exception message reaches the client
  • The generic rescue StandardError => e in JsonRpcHandler.process_request likewise responds with data: e.message

Why it matters

Exception messages routinely contain internals: class, method and variable names, and — for HTTP client errors raised inside tools (e.g. Faraday) — internal hostnames and URLs. For publicly reachable servers these leak to untrusted clients. The SDK already avoids this on its other error paths: the generic request-handler wrap uses a fixed "Internal error handling #{method} request" without the exception message, and the StreamableHTTP transport's catch-all answers with a plain "Internal server error"call_tool is the one place that embeds e.message. This is a classic information-exposure-through-error-messages weakness (CWE-209).

Suggested fix

Drop the : #{e.message} suffix from call_tool's wrap, so the client-facing message (and the data member echoing it) is just "Internal error calling tool #{tool_name}" — the tool name is useful to the caller and safe to expose. The generic rescue StandardError fallback in JsonRpcHandler.process_request should likewise stop echoing e.message into data; capability errors ("Server does not support ..."), which currently rely on that fallback for their client-facing message, keep it by re-raising through RequestHandlerError — the channel whose message is deliberately surfaced. Observability is unaffected: the original exception already reaches configuration.exception_reporter via original_error before the response is built.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions