Skip to content

fix(mcp): accept boolean JSON Schema property schemas on tools/list - #101810

Closed
littlestar030 wants to merge 1 commit into
NousResearch:hermes/simplify-codebasefrom
littlestar030:fix/mcp-boolean-property-schema-101669
Closed

littlestar030 wants to merge 1 commit into
NousResearch:hermes/simplify-codebasefrom
littlestar030:fix/mcp-boolean-property-schema-101669

Conversation

@littlestar030

@littlestar030 littlestar030 commented Sep 3, 2026 •

Copy link
Copy Markdown

What does this PR do?

One MCP tool whose inputSchema.properties uses a JSON Schema boolean sub-schema ("refresh": true) makes Hermes fail ListToolsResult validation on negotiated pre-2026 sessions and park the entire server. Boolean schemas are legal in JSON Schema 2020-12; mcp 2.0.0's 2025-11-25 wire models typed property values as objects only.

This applies the same widening upstream landed in mcp 2.1.0 (modelcontextprotocol/python-sdk#3354) at Hermes SDK import time, without bumping the pinned mcp==2.0.0.

Related Issue

Fixes #101669

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Security fix
  • Documentation update
  • Tests (adding or improving test coverage)
  • Refactor (no behavior change)
  • New skill (bundled or hub)

Changes Made

  • tools/mcp_tool.py: _patch_mcp_boolean_property_schemas() called from _ensure_mcp_sdk(); widens 2025-11-25 InputSchema/OutputSchema.properties, rebuilds nested models, clears methods._adapter cache; no-op on mcp>=2.1.0
  • tests/tools/test_mcp_boolean_property_schema.py: regression covering true/false property schemas via validate_server_result("tools/list", "2025-11-25", ...) for both inputSchema and outputSchema, including dump/validate round-trip

How to Test

  1. uv sync --extra mcp --extra dev
  2. python -m pytest tests/tools/test_mcp_boolean_property_schema.py -q
  3. Optional: connect an MCP server that advertises "properties": {"refresh": true} and confirm the server loads instead of parking with dict_type

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature
  • I've run the focused pytest file above and it passes
  • I've added tests for my changes
  • I've tested on my platform: Windows 11

Documentation & Housekeeping

  • I've updated relevant documentation — N/A (compat shim + docstring on the helper)
  • cli-config.yaml.example — N/A
  • CONTRIBUTING.md / AGENTS.md — N/A
  • Cross-platform impact considered — N/A (pure schema validation)
  • Tool descriptions/schemas — N/A

Review follow-ups

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tools Tool registry, model_tools, toolsets tool/mcp MCP client and OAuth sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Sep 3, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: #101804 addresses #101669 through input normalization and per-tool isolation; this PR patches the pinned SDK's generated validation models. They are competing mechanisms, not duplicates.

@littlestar030

Copy link
Copy Markdown
Author

This was generated by AI during triage.

Related: #101804 addresses #101669 through input normalization and per-tool isolation; this PR patches the pinned SDK's generated validation models. They are competing mechanisms, not duplicates.

Thanks for the note — agreed these are competing mechanisms, not duplicates.

This PR matches the upstream mcp fix: accept boolean property sub-schemas as-is on the pinned 2.0.0 models (no-op once bumped). #101804 rewrites them to dicts and adds a stream guard for per-tool isolation. Happy to defer to maintainers on which fits better.

hyeonsang010716 added a commit to hyeonsang010716/hermes-agent that referenced this pull request Sep 3, 2026
… server

The mcp 2.x SDK validates each tools/list page as a whole against the
negotiated protocol version's wire schema before the client sees it
(send_request runs mcp_types.methods.validate_server_result first), so one
tool the schema rejects fails the entire response. Hermes then parked the
server: in NousResearch#101669 one tool out of 312 took every other tool with it, and
the log identified the offender only by index (tools.291).

Accepting the shape that tripped the reporter — a boolean property
subschema, legal JSON Schema — is the SDK model's job: upstream widened it
(modelcontextprotocol/python-sdk#3354) and NousResearch#101810 carries that widening
for the pinned mcp==2.0.0. This change is the complementary layer the
reporter asked for independently: whatever the SDK still rejects should
cost that one tool, not the server.

Add tools/mcp_listing_guard.py, a read-stream proxy placed between the
transport and ClientSession. For each tools/list page it validates every
tool on its own against the SDK's versioned Tool model (derived from the
same SERVER_RESULTS entry the SDK validates with, so it tracks any
widening applied to that model), drops only the tools that fail, and logs
each by name with the failing path. A broken page-level field neither
hides an invalid tool nor condemns a valid one. Verdicts are latched per
connection so a keepalive that falls back to list_tools does not repeat
the same WARNING every tick, and anything unexpected passes through
untouched, so behaviour is never worse than the SDK's own.

MCPServerTask._guard_read_stream wraps the stream at all four
ClientSession sites (stdio, SSE, streamable HTTP on both SDK generations),
which covers every list_tools caller at once: discovery, tools/list_changed
refreshes, the keepalive fallback and `hermes mcp test`. The guard reads
the negotiated protocol_version off the live session and, before that is
available, falls back to the protocolVersion the server announced in the
handshake response that passed through the same stream. On mcp 1.x, which
has no per-version surface, it is inert. Dropped tools are recorded on the
task (reset on every discovery or refresh) and surfaced as dropped_tools
in get_mcp_status(), so a connected server no longer looks perfectly
healthy while a tool went missing.

Tests drive a real ClientSession over in-memory streams against a fake
server: the premise test pins that the SDK rejects the whole page without
the guard, and the guarded run keeps the valid tools while dropping the
invalid one by name. The boolean-schema verdict test tracks whether the
installed model accepts that shape, so it holds with or without NousResearch#101810.
The streamable-HTTP arity test now asserts the read stream is handed over
wrapped rather than by identity.

Fixes NousResearch#101669

@andrexibiza andrexibiza left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head review on 865d94711b94a26a3f1551afa7d319fe0e3d1764.

The compat mechanism is directionally correct: it widens the same 2025-11-25 InputSchema.properties and OutputSchema.properties types that upstream python-sdk#3354 widened, rebuilds the dependent models, and clears the cached methods adapter before tools/list validation. The regression also reaches the real versioned mcp_types.methods.validate_server_result("tools/list", "2025-11-25", ...) path rather than only testing Hermes normalization.

Two gates remain before this is landing-ready:

  1. Prove the complete compatibility surface this patch changes. The implementation deliberately mutates both InputSchema.properties and OutputSchema.properties, and upstream #3354 carries a regression for both. This PR's wire-level fixture exercises boolean sub-schemas only under inputSchema; outputSchema is never present. Add an outputSchema containing both true and false property schemas to the versioned tools/list fixture and assert that it survives validation/round-trip. That closes the second half of the actual patch instead of leaving it dependent on inference from the input path.

  2. Recompose onto the live MCP owner graph instead of extending the old godfile. This head adds 90 lines to tools/mcp_tool.py, which is still >9,400 lines on this object (line 9,450 exists). Current-main-based #102117 (aec50eb5979afb08af9c1c0d5e6a869412d2af47, base exactly main@63279301bcbdc185c1b07b98a9312eb0c862f26d) is an active structural carrier that rewrites tools/mcp_tool.py to a 629-line facade and moves MCP responsibilities into bounded sibling modules. This PR and #102117 therefore have a direct owner collision, and old-monolith proof cannot transfer across that rewrite. The landing-safe order is #102117 first, then rebase/recompose this compatibility shim into a bounded MCP compat owner with only the minimal SDK-import hook left in the facade, followed by fresh exact-head proof.

Hosted proof is also not complete for this sole surviving commit: CI 33707140527 is failure with zero dispatched jobs; Docker 33707140402 and Nix 33707140551 are action_required; all label-rerun objects are also action_required. A direct rerun of 33707140527 from the available integration returns 403 Resource not accessible by integration, so there is no green hosted receipt to transfer.

I would re-review the recomposed exact head after the output-schema regression and fresh hosted matrix exist.

@littlestar030

Copy link
Copy Markdown
Author

Thanks for the exact-head review.

  1. outputSchema coverage — done on a84f976. The versioned tools/list fixture now includes an outputSchema with both true and false property schemas (plus a normal object property). Asserts cover validate_server_result("tools/list", "2025-11-25", ...), ListToolsResult validation, and dump/by_alias round-trip; the idempotent test carries the same outputSchema shape. Local: pytest tests/tools/test_mcp_boolean_property_schema.py -q → 3 passed.

  2. Recompose onto refactor: whole-codebase simplification — −34% source LOC, every god file decomposed, zero behavior change #102117 owners — acknowledged. I'll keep the current shim as-is until refactor: whole-codebase simplification — −34% source LOC, every god file decomposed, zero behavior change #102117 lands on main, then rebase/recompose into a bounded MCP compat owner with only the SDK-import hook left on the facade, and reacquire exact-head proof there.

  3. Hosted proof — still blocked on the fork-PR approval gate (3 workflows awaiting approval / maintainer "Approve and run"); I can't dispatch CI/Docker/Nix from this side. Happy to re-request review once those exact-head workflows are admitted (and again after the refactor: whole-codebase simplification — −34% source LOC, every god file decomposed, zero behavior change #102117 recompose).

@andrexibiza andrexibiza left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head a84f9766469dcde7c06325a053c0b4012a101bf3 after the requested test follow-up.

The missing compatibility proof is now closed: the real versioned tools/list fixture carries true and false boolean sub-schemas under outputSchema.properties, asserts them after ListToolsResult validation, round-trips them through the wire aliases, and revalidates the dumped result. The idempotency test now covers both input and output boolean schemas as well. I found no new defect in the two-file compatibility slice.

Two landing gates remain, both source-of-truth rather than inferred from the old head:

  • #102117 is still open and is now at e293872ac5cc79a3ce74f5e01a6f28b9d946946e; it rewrites tools/mcp_tool.py from the old ~9.4K owner into the 629-line facade. This PR still targets the pre-extraction monolith, so it must recompose after that owner lands (or otherwise carry the compat mechanism into the bounded owner) and rerun the wire regression on the composed object.
  • Hosted exact-object proof is still absent for the new tip: CI 33789811558, Docker 33789810264, and Nix 33789810319 are all action_required. The surviving first commit also remains the previously unproved object, so the two-commit train is not every-commit green.

The implementation/test blocker from my prior review is therefore resolved; the requested next object is the bounded-owner recompose with green hosted proof for every surviving commit.

…102117 owners

Move the mcp 2.0.0 InputSchema/OutputSchema widening into mcp_tool_schema and keep only the SDK-import hook on the facade. Retargets NousResearch#101669 proof for the post-extraction MCP layout.

Co-authored-by: Cursor <[email protected]>
@littlestar030
littlestar030 force-pushed the fix/mcp-boolean-property-schema-101669 branch from a84f976 to 7bdbdac Compare September 4, 2026 07:50
@littlestar030
littlestar030 requested a review from a team September 4, 2026 07:50
@littlestar030
littlestar030 changed the base branch from main to hermes/simplify-codebase September 4, 2026 07:50
@littlestar030

Copy link
Copy Markdown
Author

Recomposed onto the live #102117 MCP owner graph (exact head based on hermes/simplify-codebase @ 27a4023).

  • Compat widening now lives in tools/mcp_tool_schema.py (bounded owner).
  • Facade tools/mcp_tool.py keeps only the _ensure_mcp_sdk import-time hook.
  • Wire regression retained: versioned tools/list fixture covers boolean true/false under both inputSchema and outputSchema, plus ListToolsResult / by_alias round-trip and idempotency.
  • Local: pytest tests/tools/test_mcp_boolean_property_schema.py -q → 3 passed.

PR base retargeted to hermes/simplify-codebase so the diff is only this compatibility slice (7bdbdac, +235 / 3 files). Happy to recompose again once #102117 lands on main if the owner paths move.

Hosted CI/Docker/Nix are still behind the fork-PR approval gate on my side — please Approve and run when convenient so we can collect exact-head hosted proof.

@teknium1
teknium1 deleted the branch NousResearch:hermes/simplify-codebase September 4, 2026 12:50
@teknium1 teknium1 closed this Sep 4, 2026
littlestar030 added a commit to littlestar030/hermes-agent that referenced this pull request Sep 4, 2026
mcp 2.0.0's 2025-11-25 InputSchema/OutputSchema reject properties.x: true/false,
parking the whole server. Widen the wire models in mcp_tool_schema and hook from
_ensure_mcp_sdk (no-op on mcp>=2.1.0). Follow-up to NousResearch#101810 after NousResearch#102117 landed.
Related to NousResearch#101669 (close with complementary NousResearch#101804 isolation).
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 P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades 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.

MCP: boolean property schema (properties.x: true) fails ListToolsResult validation and disables the entire server

4 participants