Files
AstrBot/tests/unit/test_mcp_client_schema.py
Stable Genius 43989471e1 fix: normalize invalid MCP required flags in MCP schemas (#6077)
* fix: normalize invalid MCP required flags

* style: format mcp schema normalization tests

* style: sort mcp client imports

* fix: preserve nested mcp required flags

* test: cover malformed mcp required fields

---------

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: エイカク <1259085392z@gmail.com>
Co-authored-by: 邹永赫 <1259085392@qq.com>
2026-04-20 13:30:04 +09:00

118 lines
4.2 KiB
Python

from types import SimpleNamespace
from unittest.mock import MagicMock
from astrbot.core.agent.mcp_client import MCPTool, _normalize_mcp_input_schema
class TestNormalizeMcpInputSchema:
def test_lifts_property_level_required_booleans_to_parent_required_array(self):
schema = {
"type": "object",
"properties": {
"stock_code": {"type": "string", "required": True},
"market": {"type": "string", "required": False},
},
}
normalized = _normalize_mcp_input_schema(schema)
assert normalized["required"] == ["stock_code"]
assert "required" not in normalized["properties"]["stock_code"]
assert "required" not in normalized["properties"]["market"]
assert schema["properties"]["stock_code"]["required"] is True
def test_preserves_existing_required_arrays_while_fixing_nested_objects(self):
schema = {
"type": "object",
"required": ["server"],
"properties": {
"server": {
"type": "object",
"required": ["transport"],
"properties": {
"transport": {"type": "string"},
"stock_code": {"type": "string", "required": True},
"market": {"type": "string", "required": False},
},
}
},
}
normalized = _normalize_mcp_input_schema(schema)
assert normalized["required"] == ["server"]
assert normalized["properties"]["server"]["required"] == [
"transport",
"stock_code",
]
assert (
"required"
not in normalized["properties"]["server"]["properties"]["stock_code"]
)
assert (
"required" not in normalized["properties"]["server"]["properties"]["market"]
)
def test_preserves_parent_required_flag_for_nested_object_properties(self):
schema = {
"type": "object",
"properties": {
"server": {
"type": "object",
"required": True,
"properties": {
"transport": {"type": "string", "required": True},
},
}
},
}
normalized = _normalize_mcp_input_schema(schema)
assert normalized["required"] == ["server"]
assert normalized["properties"]["server"]["required"] == ["transport"]
assert (
"required"
not in normalized["properties"]["server"]["properties"]["transport"]
)
def test_ignores_non_boolean_required_values_and_non_dict_properties(self):
schema = {
"type": "object",
"properties": {
"server": "invalid-property-schema",
"market": {"type": "string", "required": "yes"},
"stock_code": {"type": "string", "required": True},
},
}
normalized = _normalize_mcp_input_schema(schema)
assert normalized["required"] == ["stock_code"]
assert normalized["properties"]["server"] == "invalid-property-schema"
assert normalized["properties"]["market"]["required"] == "yes"
assert "required" not in normalized["properties"]["stock_code"]
assert schema["properties"]["server"] == "invalid-property-schema"
assert schema["properties"]["market"]["required"] == "yes"
class TestMCPToolSchemaNormalization:
def test_mcp_tool_accepts_property_level_required_booleans(self):
mcp_tool = SimpleNamespace(
name="quote_lookup",
description="Lookup a quote",
inputSchema={
"type": "object",
"properties": {
"stock_code": {"type": "string", "required": True},
"market": {"type": "string", "required": False},
},
},
)
tool = MCPTool(mcp_tool, MagicMock(), "gf-securities")
assert tool.parameters["required"] == ["stock_code"]
assert "required" not in tool.parameters["properties"]["stock_code"]
assert "required" not in tool.parameters["properties"]["market"]