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) <noreply@anthropic.com>

* docs: update crates/rmcp/src/handler/server.rs

* docs: update crates/rmcp/src/model.rs

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com>
This commit is contained in:
Greg Virgin 2026-06-16 22:13:33 -04:00 committed by GitHub
parent 95a8e961e0
commit 4b82e41522
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 77 additions and 1 deletions

View file

@ -269,6 +269,34 @@ macro_rules! server_handler_methods {
McpError::method_not_found::<UnsubscribeRequestMethod>(),
))
}
/// 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,

View file

@ -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<CallToolResult, ErrorData> {
/// // 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<Content>) -> Self {
CallToolResult {
content,