From 4b82e41522a70468888c26944717201285d08563 Mon Sep 17 00:00:00 2001 From: Greg Virgin Date: Tue, 16 Jun 2026 22:13:33 -0400 Subject: [PATCH] docs(server): document Err vs Ok(CallToolResult::error) visibility contract on ServerHandler::call_tool (#854) * 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) * docs: update crates/rmcp/src/handler/server.rs * docs: update crates/rmcp/src/model.rs --------- Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com> --- crates/rmcp/src/handler/server.rs | 28 +++++++++++++++++ crates/rmcp/src/model.rs | 50 ++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/crates/rmcp/src/handler/server.rs b/crates/rmcp/src/handler/server.rs index e6c3cc8..aea5967 100644 --- a/crates/rmcp/src/handler/server.rs +++ b/crates/rmcp/src/handler/server.rs @@ -269,6 +269,34 @@ 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`]), 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 + /// ([`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 7ade843..5fb7eb9 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -2844,7 +2844,55 @@ 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, 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,