Skip to content
20 changes: 6 additions & 14 deletions crates/mcpmux-gateway/src/mcp/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -860,15 +860,13 @@ impl ServerHandler for McpMuxGatewayHandler {
.await
.map_err(|e| McpError::internal_error(format!("Tool call failed: {}", e), None))?;

// Convert ToolCallResult to MCP CallToolResult
let content: Vec<Content> = tool_result
.content
.into_iter()
.filter_map(|v| serde_json::from_value(v).ok())
.collect();
// Convert ToolCallResult to MCP CallToolResult without dropping
// structuredContent or protocol-level _meta from the upstream server.
let result = tool_result.into_mcp_result();

// Log result summary - show content types and approximate sizes
let content_summary: Vec<String> = content
let content_summary: Vec<String> = result
.content
.iter()
.map(|c| {
// Content is Annotated<RawContent>, serialize to inspect type
Expand Down Expand Up @@ -907,17 +905,11 @@ impl ServerHandler for McpMuxGatewayHandler {
.collect();
debug!(
tool = %params.name,
is_error = tool_result.is_error,
is_error = result.is_error.unwrap_or(false),
content = ?content_summary,
"call_tool result"
);

let result = if tool_result.is_error {
CallToolResult::error(content)
} else {
CallToolResult::success(content)
};

Ok(result)
}

Expand Down
72 changes: 61 additions & 11 deletions crates/mcpmux-gateway/src/pool/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::time::Duration;

use anyhow::{anyhow, Result};
use mcpmux_core::{FeatureType, LogLevel, LogSource, ServerLog, ServerLogManager};
use rmcp::model::CallToolRequestParams;
use rmcp::model::{CallToolRequestParams, CallToolResult, Content, Meta};
use serde_json::Value;
use tracing::{debug, info, warn};
use uuid::Uuid;
Expand Down Expand Up @@ -52,6 +52,39 @@ pub struct RoutedResource {
pub struct ToolCallResult {
pub content: Vec<Value>,
pub is_error: bool,
pub structured_content: Option<Value>,
pub meta: Option<Meta>,
}

impl ToolCallResult {
fn from_mcp_result(result: CallToolResult) -> Self {
Self {
content: result
.content
.into_iter()
.map(|item| serde_json::to_value(item).unwrap_or(Value::Null))
.collect(),
is_error: result.is_error.unwrap_or(false),
structured_content: result.structured_content,
meta: result.meta,
}
}

pub(crate) fn into_mcp_result(self) -> CallToolResult {
let content: Vec<Content> = self
.content
.into_iter()
.filter_map(|item| serde_json::from_value(item).ok())
.collect();
let mut result = if self.is_error {
CallToolResult::error(content)
} else {
CallToolResult::success(content)
};
result.structured_content = self.structured_content;
result.meta = self.meta;
result
}
}

/// Default timeout for MCP tool calls (60 seconds)
Expand Down Expand Up @@ -284,16 +317,7 @@ impl RoutingService {
.map_err(|_| anyhow!("Tool call timed out after {:?}", TOOL_CALL_TIMEOUT))?
.map_err(|e| anyhow!("MCP call failed: {}", e))?;

let content: Vec<Value> = res
.content
.into_iter()
.map(|c| serde_json::to_value(c).unwrap_or(Value::Null))
.collect();

Ok(ToolCallResult {
content,
is_error: res.is_error.unwrap_or(false),
})
Ok(ToolCallResult::from_mcp_result(res))
}
None => Err(anyhow!("Server instance has no active client")),
}
Expand Down Expand Up @@ -726,3 +750,29 @@ impl RoutingService {
false
}
}

#[cfg(test)]
mod tests {
use super::ToolCallResult;
use rmcp::model::{CallToolResult, Content, Meta};
use serde_json::json;

#[test]
fn tool_result_round_trip_preserves_structured_content_and_meta() {
let structured = json!({ "matches": [{ "message": "found" }] });
let mut meta = Meta::new();
meta.0.insert("traceId".to_string(), json!("trace-123"));

let mut upstream = CallToolResult::structured(structured.clone());
upstream.content = vec![Content::text("search completed")];
upstream.meta = Some(meta.clone());

let routed = ToolCallResult::from_mcp_result(upstream);
let forwarded = routed.into_mcp_result();

assert_eq!(forwarded.content, vec![Content::text("search completed")]);
assert_eq!(forwarded.structured_content, Some(structured));
assert_eq!(forwarded.meta, Some(meta));
assert_eq!(forwarded.is_error, Some(false));
}
}