From b4a8ba5bbea3cf04eaea3449c8805732ec10dc9c Mon Sep 17 00:00:00 2001 From: Greg Virgin Date: Sat, 25 Apr 2026 02:29:00 +0000 Subject: [PATCH] docs(server): document Err vs Ok(CallToolResult::error) visibility contract 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) --- crates/rmcp/src/handler/server.rs | 29 +++++++++++++++++ crates/rmcp/src/model.rs | 53 ++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/crates/rmcp/src/handler/server.rs b/crates/rmcp/src/handler/server.rs index 8673a8bf..644e6dca 100644 --- a/crates/rmcp/src/handler/server.rs +++ b/crates/rmcp/src/handler/server.rs @@ -258,6 +258,35 @@ macro_rules! server_handler_methods { McpError::method_not_found::(), )) } + /// Handle a `tools/call` request from a client. + /// + /// # Choosing a return value + /// + /// MCP distinguishes two failure modes; the API forces you to pick + /// the right one explicitly because they reach the caller's UI very + /// differently: + /// + /// - `Ok(`[`CallToolResult::error`]`(...))` — the tool ran (or tried + /// to) and produced a failure the caller should see. The + /// `content` you supply is rendered in the caller's MCP client, + /// so the user gets your message. **This is the right return + /// value for almost every "the tool didn't work" path** — empty + /// results, validation failures the user can fix, downstream + /// service unavailability, etc. + /// + /// - `Err(`[`McpError`]`)` — a JSON-RPC protocol error. Use this + /// only when the request itself is unroutable: unknown tool + /// ([`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::INTERNAL_ERROR`], `-32603`). MCP clients + /// typically render protocol errors opaquely; **the caller will + /// not see your message** — they see something like "Tool result + /// missing due to internal error". If you want the caller to read + /// your error, use `Ok(CallToolResult::error(...))`. + /// + /// See [`CallToolResult::error`] for a worked example. fn call_tool( &self, request: CallToolRequestParams, diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 4aabab1d..d7ef1541 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -2838,7 +2838,58 @@ impl CallToolResult { meta: None, } } - /// Create an error tool result with unstructured content + + /// Create a tool-level error result with caller-visible content. + /// + /// # When to use this vs `Err(ErrorData)` + /// + /// MCP distinguishes two failure modes for a `call_tool` invocation, and + /// the right one to use depends on **whose problem it is**: + /// + /// - **Tool-level error** — `Ok(CallToolResult::error(...))`. + /// The request was valid and routed to your tool, but executing the + /// tool failed in a way the caller should see (a query returned no + /// rows, an external API returned 500, the user's input is plausible + /// but produced no result, etc.). The caller's MCP client renders the + /// `content` you provide; your message reaches the user. **This is the + /// right choice for almost every "the tool ran and didn't work" case.** + /// + /// - **Protocol error** — `Err(ErrorData)` with a JSON-RPC code. + /// 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 + /// ([`ErrorCode::INTERNAL_ERROR`], `-32603`). MCP clients typically + /// render protocol errors opaquely (e.g. "Tool result missing due to + /// internal error") — the caller does **not** see your message. + /// + /// # Example + /// + /// ```rust,ignore + /// use rmcp::model::{CallToolResult, Content, ErrorData}; + /// + /// async fn lookup(query: &str) -> Result { + /// // Caller passed a malformed query — the server can't run anything. + /// // This is a protocol error, the caller's client will render it + /// // as -32602 invalid_params: + /// if query.is_empty() { + /// return Err(ErrorData::invalid_params("query must be non-empty", None)); + /// } + /// + /// // Tool ran, no result. Caller should see the explanation: + /// let rows = run_query(query).await; + /// if rows.is_empty() { + /// return Ok(CallToolResult::error(vec![Content::text( + /// format!("no rows matched '{query}'"), + /// )])); + /// } + /// + /// Ok(CallToolResult::success(vec![Content::text(format_rows(&rows))])) + /// } + /// # async fn run_query(_: &str) -> Vec<&'static str> { vec![] } + /// # fn format_rows(_: &[&str]) -> String { String::new() } + /// ``` pub fn error(content: Vec) -> Self { CallToolResult { content,