Files
hkuds--openharness/tests/test_tools/test_mcp_tool.py
T
siaochuan 6510a820a7 fix(mcp): use JSON Schema types in tool input models; exclude None from MCP calls
When non-Anthropic models (MiniMax, Moonshot, etc.) call MCP tools, they
sometimes send `null` for parameters they consider optional. The previous
implementation typed all fields as `object | None`, so:

1. Pydantic accepted `null` even for required fields (e.g. `query`)
2. `model_dump()` serialized these as `{"query": null, ...}`
3. The MCP server received null for required parameters → validation error

Fix:
- Map JSON Schema types to proper Python types (string→str, integer→int,
  etc.) so Pydantic rejects null for required fields
- Use `exclude_none=True` in model_dump to strip optional null parameters
  before passing to the MCP server

This affects ALL MCP tool servers, not just specific ones.

Tested with: MiniMax-M2.7-highspeed + dongtian MCP server (15 tools).
2026-04-08 11:13:09 +08:00

95 lines
3.2 KiB
Python

"""Tests for MCP tool adapters — input model generation and argument serialization."""
import pytest
from pydantic import ValidationError
from openharness.tools.mcp_tool import _input_model_from_schema
class TestInputModelFromSchema:
"""Verify _input_model_from_schema maps JSON Schema types correctly."""
def test_required_string_rejects_none(self):
schema = {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
}
Model = _input_model_from_schema("search", schema)
with pytest.raises(ValidationError):
Model(query=None)
def test_required_string_accepts_value(self):
schema = {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
}
Model = _input_model_from_schema("search", schema)
m = Model(query="zigzag")
assert m.query == "zigzag"
def test_optional_string_defaults_to_none(self):
schema = {
"type": "object",
"properties": {
"query": {"type": "string"},
"wing": {"type": "string"},
},
"required": ["query"],
}
Model = _input_model_from_schema("search", schema)
m = Model(query="test")
assert m.wing is None
def test_exclude_none_omits_optional_keeps_required(self):
schema = {
"type": "object",
"properties": {
"query": {"type": "string"},
"wing": {"type": "string"},
"limit": {"type": "integer"},
},
"required": ["query"],
}
Model = _input_model_from_schema("search", schema)
m = Model(query="test")
dumped = m.model_dump(mode="json", exclude_none=True)
assert dumped == {"query": "test"}
def test_all_json_types_mapped(self):
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"count": {"type": "integer"},
"score": {"type": "number"},
"active": {"type": "boolean"},
"tags": {"type": "array"},
"meta": {"type": "object"},
},
"required": ["name", "count", "score", "active", "tags", "meta"],
}
Model = _input_model_from_schema("full", schema)
m = Model(name="x", count=1, score=0.5, active=True, tags=["a"], meta={"k": "v"})
dumped = m.model_dump(mode="json")
assert dumped == {
"name": "x", "count": 1, "score": 0.5,
"active": True, "tags": ["a"], "meta": {"k": "v"},
}
def test_empty_schema_creates_valid_model(self):
Model = _input_model_from_schema("empty", {"type": "object"})
m = Model()
assert m.model_dump(mode="json") == {}
def test_model_rejects_null_for_required_integer(self):
schema = {
"type": "object",
"properties": {"limit": {"type": "integer"}},
"required": ["limit"],
}
Model = _input_model_from_schema("limited", schema)
with pytest.raises(ValidationError):
Model(limit=None)