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,