fix: Add schema-aware numeric coercion for MCP tool arguments (#5478)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user