Skip to content

Commit bbd6b72

Browse files
XuleileonMohammod Al Amin Ashik
authored andcommitted
fix(gateway): preserve structured tool results
1 parent 049de49 commit bbd6b72

2 files changed

Lines changed: 67 additions & 25 deletions

File tree

crates/mcpmux-gateway/src/mcp/handler.rs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -860,15 +860,13 @@ impl ServerHandler for McpMuxGatewayHandler {
860860
.await
861861
.map_err(|e| McpError::internal_error(format!("Tool call failed: {}", e), None))?;
862862

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

870867
// Log result summary - show content types and approximate sizes
871-
let content_summary: Vec<String> = content
868+
let content_summary: Vec<String> = result
869+
.content
872870
.iter()
873871
.map(|c| {
874872
// Content is Annotated<RawContent>, serialize to inspect type
@@ -907,17 +905,11 @@ impl ServerHandler for McpMuxGatewayHandler {
907905
.collect();
908906
debug!(
909907
tool = %params.name,
910-
is_error = tool_result.is_error,
908+
is_error = result.is_error.unwrap_or(false),
911909
content = ?content_summary,
912910
"call_tool result"
913911
);
914912

915-
let result = if tool_result.is_error {
916-
CallToolResult::error(content)
917-
} else {
918-
CallToolResult::success(content)
919-
};
920-
921913
Ok(result)
922914
}
923915

crates/mcpmux-gateway/src/pool/routing.rs

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::time::Duration;
1212

1313
use anyhow::{anyhow, Result};
1414
use mcpmux_core::{FeatureType, LogLevel, LogSource, ServerLog, ServerLogManager};
15-
use rmcp::model::CallToolRequestParams;
15+
use rmcp::model::{CallToolRequestParams, CallToolResult, Content, Meta};
1616
use serde_json::Value;
1717
use tracing::{debug, info, warn};
1818
use uuid::Uuid;
@@ -52,6 +52,39 @@ pub struct RoutedResource {
5252
pub struct ToolCallResult {
5353
pub content: Vec<Value>,
5454
pub is_error: bool,
55+
pub structured_content: Option<Value>,
56+
pub meta: Option<Meta>,
57+
}
58+
59+
impl ToolCallResult {
60+
fn from_mcp_result(result: CallToolResult) -> Self {
61+
Self {
62+
content: result
63+
.content
64+
.into_iter()
65+
.map(|item| serde_json::to_value(item).unwrap_or(Value::Null))
66+
.collect(),
67+
is_error: result.is_error.unwrap_or(false),
68+
structured_content: result.structured_content,
69+
meta: result.meta,
70+
}
71+
}
72+
73+
pub(crate) fn into_mcp_result(self) -> CallToolResult {
74+
let content: Vec<Content> = self
75+
.content
76+
.into_iter()
77+
.filter_map(|item| serde_json::from_value(item).ok())
78+
.collect();
79+
let mut result = if self.is_error {
80+
CallToolResult::error(content)
81+
} else {
82+
CallToolResult::success(content)
83+
};
84+
result.structured_content = self.structured_content;
85+
result.meta = self.meta;
86+
result
87+
}
5588
}
5689

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

287-
let content: Vec<Value> = res
288-
.content
289-
.into_iter()
290-
.map(|c| serde_json::to_value(c).unwrap_or(Value::Null))
291-
.collect();
292-
293-
Ok(ToolCallResult {
294-
content,
295-
is_error: res.is_error.unwrap_or(false),
296-
})
320+
Ok(ToolCallResult::from_mcp_result(res))
297321
}
298322
None => Err(anyhow!("Server instance has no active client")),
299323
}
@@ -726,3 +750,29 @@ impl RoutingService {
726750
false
727751
}
728752
}
753+
754+
#[cfg(test)]
755+
mod tests {
756+
use super::ToolCallResult;
757+
use rmcp::model::{CallToolResult, Content, Meta};
758+
use serde_json::json;
759+
760+
#[test]
761+
fn tool_result_round_trip_preserves_structured_content_and_meta() {
762+
let structured = json!({ "matches": [{ "message": "found" }] });
763+
let mut meta = Meta::new();
764+
meta.0.insert("traceId".to_string(), json!("trace-123"));
765+
766+
let mut upstream = CallToolResult::structured(structured.clone());
767+
upstream.content = vec![Content::text("search completed")];
768+
upstream.meta = Some(meta.clone());
769+
770+
let routed = ToolCallResult::from_mcp_result(upstream);
771+
let forwarded = routed.into_mcp_result();
772+
773+
assert_eq!(forwarded.content, vec![Content::text("search completed")]);
774+
assert_eq!(forwarded.structured_content, Some(structured));
775+
assert_eq!(forwarded.meta, Some(meta));
776+
assert_eq!(forwarded.is_error, Some(false));
777+
}
778+
}

0 commit comments

Comments
 (0)