Fix MCP schema normalizer mangling parameter named 'definitions' into '$defs' - #36955
Conversation
_rewrite_local_refs unconditionally renamed every dict key 'definitions'
to '$defs' to migrate draft-07 schemas to draft-2020. This swept up
user parameter names nested inside 'properties', producing schemas like
'properties.$defs' that fail Anthropic's tool-input regex
^[a-zA-Z0-9_.-]{1,64}$ with HTTP 400 (invalid_request_body).
Repro: Azure DevOps MCP's pipelines_get_builds tool exposes a Zod
parameter literally named 'definitions: z.array(...)'. The whole tool
list 400s as soon as that tool is registered.
Fix:
1. _rewrite_local_refs now tracks whether the recursion is descending
through a 'properties' dict, and skips the rename for those keys
(they're user parameter names, never schema keywords).
2. A defensive _sanitize_property_keys pass renames any remaining
'properties' keys that still fail the Anthropic regex, rewriting
'required' in lockstep and logging a warning that points at the
offending tool. This catches future MCP servers that emit weird
keys from Zod/Pydantic converters before they reach the wire.
Tests cover both branches (parameter named 'definitions' preserved,
keyword 'definitions' still rewritten), the coexistence case, the
defensive sanitizer, and the no-op-on-clean-schemas guarantee.
|
Duplicate of #30491, which fixes the same root cause — This PR is a superset (adds a defensive |
|
Thanks for the cross-link @alt-glitch — confirmed, this and #30491 fix the same root cause in If maintainers want to land #30491, I'll close this one and re-submit the defensive Either path works for me — just flagging here so reviewers can pick one and I'll rebase accordingly. |
|
Thanks for the clear reproduction and the careful regression coverage. This is an automated hermes-sweeper review. The reported behavior is already implemented on current
Closing as implemented on main. |
Problem
_normalize_mcp_input_schema(intools/mcp_tool.py) unconditionally renamed every dict key nameddefinitionsto$defs, in order to migrate draft-07 schemas to draft-2020. The recursion didn't distinguish schema keyword positions (where the rename is correct) from user parameter names nested insideproperties(where the rename is wrong).That produced schemas like:
Anthropic's tool-input validator requires property keys to match
^[a-zA-Z0-9_.-]{1,64}$, so the whole tool list is rejected with HTTP 400invalid_request_bodythe moment one offending tool is registered.Repro
Azure DevOps MCP (
@azure-devops/mcp) definespipelines_get_buildswith a Zod parameter literally nameddefinitions:After Hermes normalizes the schema, the live tool sent to Anthropic has
properties.$defs— and every subsequent assistant turn 400s with:This breaks every Anthropic-compatible model (Claude direct, Claude via Copilot, etc.) for any user with the ADO MCP enabled, and likely affects a long tail of other Zod-based MCPs whose authors used
definitionsas a parameter name.Fix
Two-part change, both in
_normalize_mcp_input_schema:Make
_rewrite_local_refsboundary-aware. It now tracks whether the recursion is descending through apropertiesdict and skips thedefinitions→$defsrename for those keys. The schema-keyword rename still happens everywhere else (root,$defs.X, nested object schemas, etc.).Add a defensive
_sanitize_property_keyspass. Anything that still fails Anthropic's regex (e.g.$weird,has spaces, unicode, over-64-char keys from some other MCP we haven't seen yet) gets renamed in place, with therequiredarray rewritten in lockstep and a warning logged that names the offending tool and key. Catches future converters before they reach the wire.Tests
tests/tools/test_mcp_tool.pygains 4 new cases:test_property_named_definitions_is_not_renamed— the direct regression for the ADO bug.test_property_named_definitions_alongside_schema_definitions— the schema-keyword form still gets rewritten when both forms appear together (covers refs into$defs).test_invalid_property_keys_are_sanitized—$weird,has spacesget renamed;requiredis rewritten too.test_valid_property_keys_are_unchanged— sanitizer is a no-op on clean schemas (includingkebab-case, dotted, snake_case).All 242 tests in
tests/tools/test_mcp_tool.pyandtests/agent/test_moonshot_schema.pypass.Live verification
Reproduced the bug end-to-end against real ADO MCP output (90 tools loaded):
mcp_ado_pipelines_get_builds(index 27):properties.$defs— fails regex.mcp_ado_pipelines_get_builds:properties.definitions(type=array, description preserved). All 121 tools in the live agent pass the Anthropic regex.Notes for reviewers
warninglevel via the module's existinglogger, so users see noisy MCPs without surprise. The warning includes the path, renamed key pair, and tool name so server authors can fix upstream.weirdand$weird) get an integer suffix (_weird,_weird_1, …).@azure-devops/mcpwould benefit from renaming the param tobuildDefinitionsor piping the result, but Hermes shouldn't break on the existing schema in the meantime.