Skip to content

Commit b4a8ba5

Browse files
gregvirgin-lsclaude
andcommitted
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>
1 parent cc66e30 commit b4a8ba5

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

crates/rmcp/src/handler/server.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,35 @@ macro_rules! server_handler_methods {
258258
McpError::method_not_found::<UnsubscribeRequestMethod>(),
259259
))
260260
}
261+
/// Handle a `tools/call` request from a client.
262+
///
263+
/// # Choosing a return value
264+
///
265+
/// MCP distinguishes two failure modes; the API forces you to pick
266+
/// the right one explicitly because they reach the caller's UI very
267+
/// differently:
268+
///
269+
/// - `Ok(`[`CallToolResult::error`]`(...))` — the tool ran (or tried
270+
/// to) and produced a failure the caller should see. The
271+
/// `content` you supply is rendered in the caller's MCP client,
272+
/// so the user gets your message. **This is the right return
273+
/// value for almost every "the tool didn't work" path** — empty
274+
/// results, validation failures the user can fix, downstream
275+
/// service unavailability, etc.
276+
///
277+
/// - `Err(`[`McpError`]`)` — a JSON-RPC protocol error. Use this
278+
/// only when the request itself is unroutable: unknown tool
279+
/// ([`ErrorCode::METHOD_NOT_FOUND`]), unparsable or
280+
/// schema-invalid parameters ([`ErrorCode::INVALID_PARAMS`],
281+
/// `-32602`), or a server-internal failure that means the server
282+
/// cannot serve any request right now
283+
/// ([`ErrorCode::INTERNAL_ERROR`], `-32603`). MCP clients
284+
/// typically render protocol errors opaquely; **the caller will
285+
/// not see your message** — they see something like "Tool result
286+
/// missing due to internal error". If you want the caller to read
287+
/// your error, use `Ok(CallToolResult::error(...))`.
288+
///
289+
/// See [`CallToolResult::error`] for a worked example.
261290
fn call_tool(
262291
&self,
263292
request: CallToolRequestParams,

crates/rmcp/src/model.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2838,7 +2838,58 @@ impl CallToolResult {
28382838
meta: None,
28392839
}
28402840
}
2841-
/// Create an error tool result with unstructured content
2841+
2842+
/// Create a tool-level error result with caller-visible content.
2843+
///
2844+
/// # When to use this vs `Err(ErrorData)`
2845+
///
2846+
/// MCP distinguishes two failure modes for a `call_tool` invocation, and
2847+
/// the right one to use depends on **whose problem it is**:
2848+
///
2849+
/// - **Tool-level error** — `Ok(CallToolResult::error(...))`.
2850+
/// The request was valid and routed to your tool, but executing the
2851+
/// tool failed in a way the caller should see (a query returned no
2852+
/// rows, an external API returned 500, the user's input is plausible
2853+
/// but produced no result, etc.). The caller's MCP client renders the
2854+
/// `content` you provide; your message reaches the user. **This is the
2855+
/// right choice for almost every "the tool ran and didn't work" case.**
2856+
///
2857+
/// - **Protocol error** — `Err(ErrorData)` with a JSON-RPC code.
2858+
/// The server cannot route the request at all: the tool name is
2859+
/// unknown ([`ErrorCode::METHOD_NOT_FOUND`]), the parameters cannot
2860+
/// be parsed or fail schema validation
2861+
/// ([`ErrorCode::INVALID_PARAMS`], `-32602`), or an infrastructure
2862+
/// error makes the server itself unusable
2863+
/// ([`ErrorCode::INTERNAL_ERROR`], `-32603`). MCP clients typically
2864+
/// render protocol errors opaquely (e.g. "Tool result missing due to
2865+
/// internal error") — the caller does **not** see your message.
2866+
///
2867+
/// # Example
2868+
///
2869+
/// ```rust,ignore
2870+
/// use rmcp::model::{CallToolResult, Content, ErrorData};
2871+
///
2872+
/// async fn lookup(query: &str) -> Result<CallToolResult, ErrorData> {
2873+
/// // Caller passed a malformed query — the server can't run anything.
2874+
/// // This is a protocol error, the caller's client will render it
2875+
/// // as -32602 invalid_params:
2876+
/// if query.is_empty() {
2877+
/// return Err(ErrorData::invalid_params("query must be non-empty", None));
2878+
/// }
2879+
///
2880+
/// // Tool ran, no result. Caller should see the explanation:
2881+
/// let rows = run_query(query).await;
2882+
/// if rows.is_empty() {
2883+
/// return Ok(CallToolResult::error(vec![Content::text(
2884+
/// format!("no rows matched '{query}'"),
2885+
/// )]));
2886+
/// }
2887+
///
2888+
/// Ok(CallToolResult::success(vec![Content::text(format_rows(&rows))]))
2889+
/// }
2890+
/// # async fn run_query(_: &str) -> Vec<&'static str> { vec![] }
2891+
/// # fn format_rows(_: &[&str]) -> String { String::new() }
2892+
/// ```
28422893
pub fn error(content: Vec<Content>) -> Self {
28432894
CallToolResult {
28442895
content,

0 commit comments

Comments
 (0)