Skip to content

fix(tools): schema-aware MCP normalization preserves property names#14977

Closed
wzahedi wants to merge 1 commit into
NousResearch:mainfrom
wzahedi:fix/mcp-schema-normalize-preserves-property-names
Closed

fix(tools): schema-aware MCP normalization preserves property names#14977
wzahedi wants to merge 1 commit into
NousResearch:mainfrom
wzahedi:fix/mcp-schema-normalize-preserves-property-names

Conversation

@wzahedi
Copy link
Copy Markdown

@wzahedi wzahedi commented Apr 24, 2026

What changed and why

_normalize_mcp_input_schema in tools/mcp_tool.py recursed blindly into every dict, treating the properties map itself as a schema node. When a tool parameter happens to share a name with a JSON Schema keyword — properties, required, or definitions — the heuristics misfire. The most visible symptom: Anthropic 400s on tool-use requests with

tools.N.custom.input_schema: JSON schema is invalid.
It must match JSON Schema draft 2020-12.
('object' is not of type 'object', 'boolean')

because Hermes had injected a stray "type": "object" sibling into the properties map. The existing check

if not repaired.get("type") and ("properties" in repaired or "required" in repaired):
    repaired["type"] = "object"

was meant to say "this node looks like an object schema" but triggers equally on a property-map whose keys happen to include properties or required. Similarly the definitions → $defs rewrite applied to a user-named property rather than only the JSON Schema keyword.

The fix replaces the two structure-blind passes (_rewrite_local_refs + _repair_object_shape) with a single schema-aware walk that recurses only into positions holding JSON Schema values:

  • Single schema: items, additionalProperties, contains, not, if/then/else, propertyNames, unevaluatedItems/unevaluatedProperties, contentSchema, additionalItems (draft-07 compat).
  • List of schemas: allOf, anyOf, oneOf, prefixItems.
  • Map of name→schema: properties, patternProperties, dependentSchemas, $defs, definitions.

Everything else (including type, required, enum, const, description, and unknown keywords) is treated as a leaf and copied verbatim. Boolean schemas (true, additionalProperties: false) pass through unchanged. No public API change; return shape is identical and walk order is still post-order.

Repro

Trivial with any Notion-shaped MCP server. The official @notionhq/notion-mcp-server defines tools like API_post_page / API_patch_page / API_create_a_data_source / API_update_a_data_source whose schemas include a top-level properties field mirroring the Notion REST API. User-authored Notion MCP servers (anywhere with z.object({ ..., properties: z.string() })) hit the same trap. In my local setup, 7 tools across two Notion MCP servers all produced schemas Anthropic rejected before this fix.

How to test it

./venv/bin/pytest tests/tools/test_mcp_tool.py -q -k "schema or normali or definitions_refs or missing_type or null_type or required_pruned or required_removed or nested_objects or array_items or property_named or additionalproperties"

New regression tests cover:

  • Parameter literally named properties — property-map stays clean (no injected type).
  • Parameter named required — not pruned / not rewritten.
  • Parameter named definitions — not renamed to $defs.
  • additionalProperties: false preserved.

Platforms tested

  • macOS 15 (Darwin 25.4.0, arm64), Python 3.11.13
  • Full tests/tools/test_mcp_tool.py: 177 of 178 pass. The one failure (TestShutdown::test_shutdown_is_parallel) is a pre-existing timing-flake unrelated to this change.
  • End-to-end: live 138-tool payload (including previously-broken Notion tools) validates clean via jsonschema.Draft202012Validator.check_schema for every tool, and a real claude-opus-4-7 request with the full tools list returns successfully.

No platform-specific code paths are touched — the change is pure Python dict/list manipulation — so Linux impact is expected to be identical.

Related issues

🤖 Generated with Claude Code

@wzahedi wzahedi marked this pull request as ready for review April 24, 2026 07:56
`_normalize_mcp_input_schema` recursed blindly into every dict, treating
the `properties` map itself as a schema node. If a tool parameter
happened to be named `properties`, `required`, or `definitions`, the
heuristics misfired — injecting a stray `"type": "object"` sibling into
the property map or renaming a property to `$defs`. Anthropic rejects
the former against JSON Schema draft 2020-12:

    tools.N.custom.input_schema: JSON schema is invalid.
    ('object' is not of type 'object', 'boolean')

Replace the two structure-blind passes with one schema-aware walk that
recurses only into positions holding JSON Schema values (items, values
of properties, members of oneOf, …) and treats everything else as a
leaf. Regression tests cover parameters named `properties`, `required`,
and `definitions`, plus preservation of `additionalProperties: false`.

Repro: any Notion-shaped MCP server (Notion's official
`@notionhq/notion-mcp-server`, user-authored servers with a `properties`
arg mirroring Notion's API) now works with Anthropic models.

Related: NousResearch#14927 (adjacent bug in the same normalization path; distinct
root cause — that issue is a malformed server schema, this is a
hermes-side heuristic misfire on a valid server schema).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wzahedi wzahedi force-pushed the fix/mcp-schema-normalize-preserves-property-names branch from 18a8015 to f172b09 Compare April 24, 2026 08:03
@wzahedi wzahedi changed the title fix(mcp): schema-aware normalization preserves property names fix(tools): schema-aware MCP normalization preserves property names Apr 24, 2026
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/tools Tool registry, model_tools, toolsets tool/mcp MCP client and OAuth labels Apr 24, 2026
@wzahedi wzahedi closed this May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P1 High — major feature broken, no workaround tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants