From 56aeb499c9df431dce47a694e773a76f67ec46f8 Mon Sep 17 00:00:00 2001 From: jaberjaber23 Date: Tue, 10 Mar 2026 01:41:33 +0300 Subject: [PATCH] fix tool-schema --- Cargo.toml | 2 +- crates/openfang-runtime/src/mcp.rs | 13 ++++++ crates/openfang-types/src/tool.rs | 63 +++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3fb63e50..0aa0c836 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.3.38" +version = "0.3.39" edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/RightNow-AI/openfang" diff --git a/crates/openfang-runtime/src/mcp.rs b/crates/openfang-runtime/src/mcp.rs index 6b492e69..f899ce3a 100644 --- a/crates/openfang-runtime/src/mcp.rs +++ b/crates/openfang-runtime/src/mcp.rs @@ -200,6 +200,19 @@ impl McpConnection { let input_schema = tool .get("inputSchema") .cloned() + .and_then(|v| { + // Ensure input_schema is a JSON object. MCP servers may + // return it as a string, null, or omit it entirely. + match &v { + serde_json::Value::Object(_) => Some(v), + serde_json::Value::String(s) => { + serde_json::from_str::(s) + .ok() + .filter(|p| p.is_object()) + } + _ => None, + } + }) .unwrap_or(serde_json::json!({"type": "object"})); // Namespace: mcp_{server}_{tool} diff --git a/crates/openfang-types/src/tool.rs b/crates/openfang-types/src/tool.rs index 351c1d84..1e45d32f 100644 --- a/crates/openfang-types/src/tool.rs +++ b/crates/openfang-types/src/tool.rs @@ -56,7 +56,20 @@ pub fn normalize_schema_for_provider( fn normalize_schema_recursive(schema: &serde_json::Value) -> serde_json::Value { let obj = match schema.as_object() { Some(o) => o, - None => return schema.clone(), + None => { + // If the schema is a JSON string, try to parse it as a JSON object. + // Some MCP servers / skill definitions serialize schemas as strings. + if let Some(s) = schema.as_str() { + if let Ok(parsed) = serde_json::from_str::(s) { + if parsed.is_object() { + return normalize_schema_recursive(&parsed); + } + } + } + // Non-object schema (null, number, bool, unparseable string, array) — + // return a valid empty object schema so providers don't reject it. + return serde_json::json!({"type": "object", "properties": {}}); + } }; let mut result = serde_json::Map::new(); @@ -258,4 +271,52 @@ mod tests { .get("$schema") .is_none()); } + + #[test] + fn test_normalize_schema_string_parsed_to_object() { + // MCP servers may return inputSchema as a JSON string + let schema = serde_json::Value::String( + r#"{"type":"object","properties":{"query":{"type":"string"}}}"#.to_string(), + ); + let result = normalize_schema_for_provider(&schema, "openai"); + assert!(result.is_object()); + assert_eq!(result["type"], "object"); + assert!(result["properties"]["query"].is_object()); + } + + #[test] + fn test_normalize_schema_null_becomes_empty_object() { + let schema = serde_json::Value::Null; + let result = normalize_schema_for_provider(&schema, "openai"); + assert!(result.is_object()); + assert_eq!(result["type"], "object"); + } + + #[test] + fn test_normalize_schema_unparseable_string_becomes_empty_object() { + let schema = serde_json::Value::String("not valid json".to_string()); + let result = normalize_schema_for_provider(&schema, "openai"); + assert!(result.is_object()); + assert_eq!(result["type"], "object"); + } + + #[test] + fn test_normalize_schema_number_becomes_empty_object() { + let schema = serde_json::json!(42); + let result = normalize_schema_for_provider(&schema, "openai"); + assert!(result.is_object()); + assert_eq!(result["type"], "object"); + } + + #[test] + fn test_normalize_schema_string_with_dollar_schema_stripped() { + // String schema that contains $schema — should be parsed AND normalized + let schema = serde_json::Value::String( + r#"{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{}}"#.to_string(), + ); + let result = normalize_schema_for_provider(&schema, "openai"); + assert!(result.is_object()); + assert_eq!(result["type"], "object"); + assert!(result.get("$schema").is_none()); + } }