Skip to main content

soma_mcp_server/
error_result.rs

1//! Reusable protocol-level tool-error result shaping.
2//!
3//! Bridges a structured JSON error payload into an MCP `CallToolResult` with
4//! `isError: true`, and builds the generic "unknown tool" protocol error
5//! every action-dispatch or router-style MCP server eventually needs.
6
7use rmcp::{
8    model::{CallToolResult, ContentBlock},
9    ErrorData,
10};
11use serde_json::{json, Value};
12
13/// Wrap a structured tool-error JSON payload into an MCP `CallToolResult`,
14/// truncating down to a small overflow envelope when the serialized payload
15/// would exceed `max_response_bytes` so callers never emit invalid,
16/// mid-value-truncated JSON.
17pub fn tool_error_result(
18    value: Value,
19    max_response_bytes: usize,
20) -> Result<CallToolResult, ErrorData> {
21    let text = serde_json::to_string(&value)
22        .map_err(|e| ErrorData::internal_error(format!("serialization error: {e}"), None))?;
23    let (payload, text) = if text.len() <= max_response_bytes {
24        (value, text)
25    } else {
26        let payload = error_overflow_payload(&value, text.len(), max_response_bytes);
27        let text = serde_json::to_string(&payload)
28            .map_err(|e| ErrorData::internal_error(format!("serialization error: {e}"), None))?;
29        (payload, text)
30    };
31    let mut result = CallToolResult::structured_error(payload);
32    result.content = vec![ContentBlock::text(text)];
33    Ok(result)
34}
35
36fn error_overflow_payload(
37    value: &Value,
38    serialized_bytes: usize,
39    max_response_bytes: usize,
40) -> Value {
41    json!({
42        "kind": "mcp_tool_error",
43        "schema_version": 1,
44        "code": "error_payload_too_large",
45        "original_kind": value.get("kind").cloned().unwrap_or(Value::Null),
46        "original_code": value.get("code").cloned().unwrap_or(Value::Null),
47        "message": "Tool error payload exceeded the MCP response size limit. The original JSON was not returned to avoid invalid truncated JSON.",
48        "retryable": true,
49        "serialized_bytes": serialized_bytes,
50        "max_response_bytes": max_response_bytes,
51        "remediation": "Retry with narrower arguments. If this repeats, inspect server logs for the original error details.",
52    })
53}
54
55/// Build a generic "unknown tool" protocol error for `call_tool`/`list_tools`
56/// dispatch, naming the offending tool and (when non-empty) the tools the
57/// server does expose.
58pub fn unknown_tool_error(tool_name: &str, available_tools: &[&str]) -> ErrorData {
59    let message = if available_tools.is_empty() {
60        format!("unknown tool: {tool_name}")
61    } else {
62        format!(
63            "unknown tool: {tool_name}; available tools: {}",
64            available_tools.join(", ")
65        )
66    };
67    ErrorData::invalid_params(
68        message,
69        Some(json!({
70            "kind": "mcp_protocol_error",
71            "schema_version": 1,
72            "code": "unknown_tool",
73            "tool": tool_name,
74            "available_tools": available_tools,
75            "retryable": true,
76            "remediation": "Call tools/list, then retry with one of the advertised tool names.",
77        })),
78    )
79}
80
81#[cfg(test)]
82#[path = "error_result_tests.rs"]
83mod tests;