diff --git a/crates/mcpmux-gateway/src/mcp/handler.rs b/crates/mcpmux-gateway/src/mcp/handler.rs index 8258ab80..b270e743 100644 --- a/crates/mcpmux-gateway/src/mcp/handler.rs +++ b/crates/mcpmux-gateway/src/mcp/handler.rs @@ -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 = 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 = content + let content_summary: Vec = result + .content .iter() .map(|c| { // Content is Annotated, serialize to inspect type @@ -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) } diff --git a/crates/mcpmux-gateway/src/pool/routing.rs b/crates/mcpmux-gateway/src/pool/routing.rs index 7a081a33..42871b3b 100644 --- a/crates/mcpmux-gateway/src/pool/routing.rs +++ b/crates/mcpmux-gateway/src/pool/routing.rs @@ -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; @@ -52,6 +52,39 @@ pub struct RoutedResource { pub struct ToolCallResult { pub content: Vec, pub is_error: bool, + pub structured_content: Option, + pub meta: Option, +} + +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 = 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) @@ -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 = 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")), } @@ -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)); + } +}