docs(server): document Err vs Ok(CallToolResult::error) visibility contract on ServerHandler::call_tool#854
Conversation
…ntract
The MCP spec separates two failure modes that surface very differently in
clients:
- Err(ErrorData) is a JSON-RPC protocol error. Most MCP clients render
it opaquely ("Tool result missing due to internal error") - the
caller does not see the message text.
- Ok(CallToolResult::error(content)) is a tool-level error. Clients
render the content; the caller reads the message.
The right shape for "the tool didn't work" is the latter, but Err is
what most handlers reach for because it looks like the natural Rust
return value. This commit adds rustdoc on both ServerHandler::call_tool
and CallToolResult::error pointing handlers at the correct shape, with
a worked example showing protocol errors (-32602 invalid_params) vs
tool errors (empty result, downstream failure).
This is the docs half of the visibility-contract ask. A follow-up may
introduce a typed ToolOutcome sum type to enforce the distinction at
compile time; this PR is the lower-risk version that unblocks the
class immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a6aa97a to
b4a8ba5
Compare
|
Heads up — CI on the latest commit ( Happy to do anything that helps unblock — no rush, just flagging in case it's silently stuck. Thanks for taking a look. |
DaleSeo
left a comment
There was a problem hiding this comment.
@gregvirgin-ls Sorry for getting to this PR late! I have two quick comments.
| /// ([`ErrorCode::METHOD_NOT_FOUND`]), unparsable or | ||
| /// schema-invalid parameters ([`ErrorCode::INVALID_PARAMS`], | ||
| /// `-32602`), or a server-internal failure that means the server | ||
| /// cannot serve any request right now |
There was a problem hiding this comment.
As PR #894 has landed, I would limit protocol errors here to requests that cannot be routed or represented as a valid tools/call.
| /// ([`ErrorCode::METHOD_NOT_FOUND`]), unparsable or | |
| /// schema-invalid parameters ([`ErrorCode::INVALID_PARAMS`], | |
| /// `-32602`), or a server-internal failure that means the server | |
| /// cannot serve any request right now | |
| /// ([`ErrorCode::METHOD_NOT_FOUND`]), malformed request shape that | |
| /// cannot be treated as a valid `tools/call`, or a server-internal | |
| /// failure that means the server cannot serve any request right now |
| /// The server cannot route the request at all: the tool name is | ||
| /// unknown ([`ErrorCode::METHOD_NOT_FOUND`]), the parameters cannot | ||
| /// be parsed or fail schema validation | ||
| /// ([`ErrorCode::INVALID_PARAMS`], `-32602`), or an infrastructure | ||
| /// error makes the server itself unusable |
There was a problem hiding this comment.
The CallToolResult::error documentation repeats the same classification and uses an empty query as an example of a protocol error. From the perspective of someone writing a handler, an empty query is the kind of user or model correctable input error that should be returned as Ok(CallToolResult::error(...)). Otherwise, this example contradicts the current specifications.
| /// The server cannot route the request at all: the tool name is | |
| /// unknown ([`ErrorCode::METHOD_NOT_FOUND`]), the parameters cannot | |
| /// be parsed or fail schema validation | |
| /// ([`ErrorCode::INVALID_PARAMS`], `-32602`), or an infrastructure | |
| /// error makes the server itself unusable | |
| /// The server cannot route the request at all, or an infrastructure | |
| /// error makes the server itself unusable |
Summary
Pure rustdoc PR. Documents on
ServerHandler::call_toolandCallToolResult::errorthe distinction between the two failure modes the MCP spec defines, since they look identical at the Rust call site but reach the caller's UI very differently:Ok(CallToolResult::error(content))— tool-level error. Thecontentis rendered in the caller's MCP client; the user sees the message. The right shape for almost every "the tool didn't work" path.Err(McpError)— JSON-RPC protocol error. Used forMETHOD_NOT_FOUND,INVALID_PARAMS(-32602),INTERNAL_ERROR(-32603), and other unroutable-request cases. Clients render these opaquely — the caller does not see the message text.The two-failure-mode shape exists in the SDK today but isn't currently explained anywhere a handler author would notice. In practice, handlers reach for
Err(...)because it looks like the natural Rust return value, and the caller sees "Tool result missing due to internal error" instead of the actual failure reason.What this PR does
crates/rmcp/src/handler/server.rs— rustdoc on thecall_toolmethod generated byserver_handler_methods!. Explains both return paths, when each is correct, and points atCallToolResult::errorfor the worked example.crates/rmcp/src/model.rs— rustdoc onCallToolResult::errorwith a worked example contrasting:Err(McpError::invalid_params(...)).Ok(CallToolResult::error(vec![Content::text("no such row")])).Total diff: 81 lines added across the two files.
What this PR does NOT do
ToolOutcomesum type to enforce the distinction at compile time (tracked as a separate follow-up); this PR is the lower-risk docs-only version that unblocks the class immediately.call_tool; once that lands, a small follow-up doc-update PR will extend the rustdoc here to cover the new behaviour. Keeping the two changes independent so each can land on its own merit.Behaviour change
None. Pure rustdoc addition.
Test matrix
(Local verification: rebased onto current
upstream/main(v1.7.0+ tip);cargo build -p rmcp --features "client,server,macros"andcargo doc -p rmcp --features "client,server,macros" --no-depsboth succeeded. Full test/clippy matrix above not run locally — left to CI.)Downstream motivation
Multiple MCP-server consumers we've worked with built up the
Err-everywhere pattern in their handlers, then had to do a sweep when they realised callers couldn't read their error messages. The docstring is the cheapest possible intervention that prevents that sweep for future implementations.