fix: Add schema-aware numeric coercion for MCP tool arguments (#5478)

This commit is contained in:
tlongwell-block
2025-11-05 10:56:17 -05:00
committed by GitHub
parent b7f5f0f97f
commit 78fd31a47a
+88 -2
View File
@@ -3,6 +3,7 @@ use std::sync::Arc;
use async_stream::try_stream;
use futures::stream::StreamExt;
use serde_json::{json, Value};
use tracing::debug;
use super::super::agents::Agent;
@@ -19,6 +20,79 @@ use crate::agents::recipe_tools::dynamic_task_tools::should_enabled_subagents;
use crate::session::SessionManager;
use rmcp::model::Tool;
fn coerce_value(s: &str, schema: &Value) -> Value {
let type_str = schema.get("type");
match type_str {
Some(Value::String(t)) => match t.as_str() {
"number" | "integer" => try_coerce_number(s),
"boolean" => try_coerce_boolean(s),
_ => Value::String(s.to_string()),
},
Some(Value::Array(types)) => {
// Try each type in order
for t in types {
if let Value::String(type_name) = t {
match type_name.as_str() {
"number" | "integer" if s.parse::<f64>().is_ok() => {
return try_coerce_number(s)
}
"boolean" if matches!(s.to_lowercase().as_str(), "true" | "false") => {
return try_coerce_boolean(s)
}
_ => continue,
}
}
}
Value::String(s.to_string())
}
_ => Value::String(s.to_string()),
}
}
fn try_coerce_number(s: &str) -> Value {
if let Ok(n) = s.parse::<f64>() {
if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
json!(n as i64)
} else {
json!(n)
}
} else {
Value::String(s.to_string())
}
}
fn try_coerce_boolean(s: &str) -> Value {
match s.to_lowercase().as_str() {
"true" => json!(true),
"false" => json!(false),
_ => Value::String(s.to_string()),
}
}
fn coerce_tool_arguments(
arguments: Option<serde_json::Map<String, Value>>,
tool_schema: &Value,
) -> Option<serde_json::Map<String, Value>> {
let args = arguments?;
let properties = tool_schema.get("properties").and_then(|p| p.as_object())?;
let mut coerced = serde_json::Map::new();
for (key, value) in args.iter() {
let coerced_value =
if let (Value::String(s), Some(prop_schema)) = (value, properties.get(key)) {
coerce_value(s, prop_schema)
} else {
value.clone()
};
coerced.insert(key.clone(), coerced_value);
}
Some(coerced)
}
async fn toolshim_postprocess(
response: Message,
toolshim_tools: &[Tool],
@@ -190,13 +264,25 @@ impl Agent {
&self,
response: &Message,
) -> (Vec<ToolRequest>, Vec<ToolRequest>, Message) {
// First collect all tool requests
let tools = self.list_tools(None).await;
// First collect all tool requests with coercion applied
let tool_requests: Vec<ToolRequest> = response
.content
.iter()
.filter_map(|content| {
if let MessageContent::ToolRequest(req) = content {
Some(req.clone())
let mut coerced_req = req.clone();
if let Ok(ref mut tool_call) = coerced_req.tool_call {
if let Some(tool) = tools.iter().find(|t| t.name == tool_call.name) {
let schema_value = Value::Object(tool.input_schema.as_ref().clone());
tool_call.arguments =
coerce_tool_arguments(tool_call.arguments.clone(), &schema_value);
}
}
Some(coerced_req)
} else {
None
}