fix(mcp): normalize nullable tool schemas

This commit is contained in:
Pony.Ma
2026-04-28 09:57:58 +08:00
committed by Teknium
parent 9cd02b1698
commit 02ae152222
4 changed files with 224 additions and 21 deletions

View File

@@ -544,6 +544,36 @@ class TestConvertTools:
assert convert_tools_to_anthropic([]) == []
assert convert_tools_to_anthropic(None) == []
def test_strips_nullable_union_from_input_schema(self):
tools = [
{
"type": "function",
"function": {
"name": "run",
"description": "Run command",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string"},
"timeout": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"default": None,
},
},
"required": ["command"],
},
},
}
]
result = convert_tools_to_anthropic(tools)
assert result[0]["input_schema"]["properties"]["timeout"] == {
"type": "integer",
"default": None,
}
assert result[0]["input_schema"]["required"] == ["command"]
# ---------------------------------------------------------------------------
# Message conversion

View File

@@ -266,6 +266,56 @@ class TestSchemaConversion:
assert schema["properties"]["items"]["items"]["properties"] == {}
def test_optional_nullable_field_is_collapsed_to_non_null_schema(self):
"""Anthropic rejects MCP/Pydantic anyOf-null optional parameter schemas."""
from tools.mcp_tool import _normalize_mcp_input_schema
schema = _normalize_mcp_input_schema({
"type": "object",
"properties": {
"command": {"type": "string"},
"workdir": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": None,
"description": "Optional working directory",
},
},
"required": ["command"],
})
assert schema["properties"]["workdir"] == {
"type": "string",
"default": None,
"description": "Optional working directory",
}
assert schema["required"] == ["command"]
def test_nested_nullable_array_items_are_collapsed(self):
from tools.mcp_tool import _normalize_mcp_input_schema
schema = _normalize_mcp_input_schema({
"type": "object",
"properties": {
"filters": {
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"properties": {"field": {"type": "string"}},
},
{"type": "null"},
]
},
}
},
})
assert schema["properties"]["filters"]["items"] == {
"type": "object",
"properties": {"field": {"type": "string"}},
}
def test_convert_mcp_schema_survives_missing_inputschema_attribute(self):
"""A Tool object without .inputSchema must not crash registration."""
import types
@@ -1910,15 +1960,38 @@ class TestUtilityToolRegistration:
import math
import time
from mcp.types import (
CreateMessageResult,
CreateMessageResultWithTools,
ErrorData,
SamplingCapability,
SamplingToolsCapability,
TextContent,
ToolUseContent,
)
class _CompatType:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
try:
from mcp.types import (
CreateMessageResult,
ErrorData,
SamplingCapability,
TextContent,
)
except ImportError:
CreateMessageResult = _CompatType
ErrorData = _CompatType
SamplingCapability = _CompatType
TextContent = _CompatType
try:
from mcp.types import CreateMessageResultWithTools
except ImportError:
CreateMessageResultWithTools = _CompatType
try:
from mcp.types import SamplingToolsCapability
except ImportError:
SamplingToolsCapability = _CompatType
try:
from mcp.types import ToolUseContent
except ImportError:
ToolUseContent = _CompatType
from tools.mcp_tool import SamplingHandler, _safe_numeric