Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,35 @@ 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`]), 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,
Expand Down
53 changes: 52 additions & 1 deletion crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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,
Expand Down