fix: normalize MCP tool schema for OpenAI-compatible providers

This commit is contained in:
haosenwang1018
2026-03-20 05:47:17 +00:00
committed by Xubin Ren
parent 9f10ce072f
commit b6cf7020ac

View File

@@ -11,6 +11,105 @@ from nanobot.agent.tools.base import Tool
from nanobot.agent.tools.registry import ToolRegistry from nanobot.agent.tools.registry import ToolRegistry
def _normalize_schema_for_openai(schema: dict[str, Any]) -> dict[str, Any]:
"""Normalize JSON Schema for OpenAI-compatible providers.
OpenAI's API (and many compatible providers) only supports a subset of JSON Schema:
- Top-level type must be 'object'
- No oneOf/anyOf/allOf/enum/not at the top level
- Properties should have simple types
"""
if not isinstance(schema, dict):
return {"type": "object", "properties": {}}
# If schema has oneOf/anyOf/allOf at top level, try to extract the first option
for key in ["oneOf", "anyOf", "allOf"]:
if key in schema:
options = schema[key]
if isinstance(options, list) and len(options) > 0:
# Use the first option as the base schema
first_option = options[0]
if isinstance(first_option, dict):
# Merge with other schema properties, preferring the first option
normalized = dict(schema)
del normalized[key]
normalized.update(first_option)
return _normalize_schema_for_openai(normalized)
# Ensure top-level type is object
if schema.get("type") != "object":
# If no type specified or different type, default to object
schema = {"type": "object", **{k: v for k, v in schema.items() if k != "type"}}
# Clean up unsupported properties at top level
unsupported = ["enum", "not", "const"]
for key in unsupported:
schema.pop(key, None)
# Ensure properties and required exist
if "properties" not in schema:
schema["properties"] = {}
if "required" not in schema:
schema["required"] = []
# Recursively normalize nested property schemas
if "properties" in schema and isinstance(schema["properties"], dict):
for prop_name, prop_schema in schema["properties"].items():
if isinstance(prop_schema, dict):
schema["properties"][prop_name] = _normalize_property_schema(prop_schema)
return schema
def _normalize_property_schema(schema: dict[str, Any]) -> dict[str, Any]:
"""Normalize a property schema for OpenAI compatibility."""
if not isinstance(schema, dict):
return {"type": "string"}
# Handle oneOf/anyOf in properties
for key in ["oneOf", "anyOf"]:
if key in schema:
options = schema[key]
if isinstance(options, list) and len(options) > 0:
first_option = options[0]
if isinstance(first_option, dict):
# Replace the complex schema with the first option
result = {k: v for k, v in schema.items() if k not in [key, "allOf", "not"]}
result.update(first_option)
return _normalize_property_schema(result)
# Handle allOf by merging all subschemas
if "allOf" in schema:
subschemas = schema["allOf"]
if isinstance(subschemas, list):
merged = {}
for sub in subschemas:
if isinstance(sub, dict):
merged.update(sub)
# Remove allOf and merge with other properties
result = {k: v for k, v in schema.items() if k != "allOf"}
result.update(merged)
return _normalize_property_schema(result)
# Ensure type is simple
if "type" not in schema:
# Try to infer type from other properties
if "enum" in schema:
schema["type"] = "string"
elif "properties" in schema:
schema["type"] = "object"
elif "items" in schema:
schema["type"] = "array"
else:
schema["type"] = "string"
# Clean up not/const
schema.pop("not", None)
schema.pop("const", None)
return schema
class MCPToolWrapper(Tool): class MCPToolWrapper(Tool):
"""Wraps a single MCP server tool as a nanobot Tool.""" """Wraps a single MCP server tool as a nanobot Tool."""
@@ -19,7 +118,8 @@ class MCPToolWrapper(Tool):
self._original_name = tool_def.name self._original_name = tool_def.name
self._name = f"mcp_{server_name}_{tool_def.name}" self._name = f"mcp_{server_name}_{tool_def.name}"
self._description = tool_def.description or tool_def.name self._description = tool_def.description or tool_def.name
self._parameters = tool_def.inputSchema or {"type": "object", "properties": {}} raw_schema = tool_def.inputSchema or {"type": "object", "properties": {}}
self._parameters = _normalize_schema_for_openai(raw_schema)
self._tool_timeout = tool_timeout self._tool_timeout = tool_timeout
@property @property