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.
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
datamember.{"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_toolwraps any unexpected exception intoRequestHandlerError.new("Internal error calling tool #{tool_name}: #{e.message}", ...)JsonRpcHandler.handle_request_errorfalls back toerror.messagefor thedatamember, so the embedded exception message reaches the clientrescue StandardError => einJsonRpcHandler.process_requestlikewise responds withdata: e.messageWhy 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_toolis the one place that embedse.message. This is a classic information-exposure-through-error-messages weakness (CWE-209).Suggested fix
Drop the
: #{e.message}suffix fromcall_tool's wrap, so the client-facing message (and thedatamember echoing it) is just"Internal error calling tool #{tool_name}"— the tool name is useful to the caller and safe to expose. The genericrescue StandardErrorfallback inJsonRpcHandler.process_requestshould likewise stop echoinge.messageintodata; capability errors ("Server does not support ..."), which currently rely on that fallback for their client-facing message, keep it by re-raising throughRequestHandlerError— the channel whose message is deliberately surfaced. Observability is unaffected: the original exception already reachesconfiguration.exception_reporterviaoriginal_errorbefore the response is built.