Skip to content

Commit 4b82e41

Browse files
gregvirgin-lsclaudeDaleSeo
authored
docs(server): document Err vs Ok(CallToolResult::error) visibility contract on ServerHandler::call_tool (modelcontextprotocol#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>
1 parent 95a8e96 commit 4b82e41

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

crates/rmcp/src/handler/server.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,34 @@ macro_rules! server_handler_methods {
269269
McpError::method_not_found::<UnsubscribeRequestMethod>(),
270270
))
271271
}
272+
/// Handle a `tools/call` request from a client.
273+
///
274+
/// # Choosing a return value
275+
///
276+
/// MCP distinguishes two failure modes; the API forces you to pick
277+
/// the right one explicitly because they reach the caller's UI very
278+
/// differently:
279+
///
280+
/// - `Ok(`[`CallToolResult::error`]`(...))` — the tool ran (or tried
281+
/// to) and produced a failure the caller should see. The
282+
/// `content` you supply is rendered in the caller's MCP client,
283+
/// so the user gets your message. **This is the right return
284+
/// value for almost every "the tool didn't work" path** — empty
285+
/// results, validation failures the user can fix, downstream
286+
/// service unavailability, etc.
287+
///
288+
/// - `Err(`[`McpError`]`)` — a JSON-RPC protocol error. Use this
289+
/// only when the request itself is unroutable: unknown tool
290+
/// ([`ErrorCode::METHOD_NOT_FOUND`]), malformed request shape that
291+
/// cannot be treated as a valid `tools/call`, or a server-internal
292+
/// failure that means the server cannot serve any request right now
293+
/// ([`ErrorCode::INTERNAL_ERROR`], `-32603`). MCP clients
294+
/// typically render protocol errors opaquely; **the caller will
295+
/// not see your message** — they see something like "Tool result
296+
/// missing due to internal error". If you want the caller to read
297+
/// your error, use `Ok(CallToolResult::error(...))`.
298+
///
299+
/// See [`CallToolResult::error`] for a worked example.
272300
fn call_tool(
273301
&self,
274302
request: CallToolRequestParams,

crates/rmcp/src/model.rs

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

0 commit comments

Comments
 (0)