You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(mcp): suppress response for JSON-RPC notifications (v1.0.656-alpha)
ADR-035 W11 fix-up wedge #12 — agy's MCP tools/call STILL failed
after v1.0.654's dot-name filter. Different error, same root family.
## Root cause: JSON-RPC 2.0 §4.1 violation
JSON-RPC 2.0 §4.1: 'The Server MUST NOT reply to a Notification.'
A notification is a request without an `id` field. Our hub's MCP
handler had a default-case error path that wrote an error frame for
every unknown method — including notifications.
agy 1.0.1 sends `notifications/roots/list_changed` to every
connected MCP server periodically (host-verified in agytest's
stderr log). Our hub fell through to default → wrote a JSON-RPC
error response. The bridge piped that to agy's stdin as an
unsolicited frame. agy's strict MCP client read the unsolicited
frame, classified it as a protocol violation, and closed the
stdio transport. Every subsequent tools/call from the LLM hit
the dead transport → 'connection closed: client is closing:
invalid request'.
The same hub also failed for any other notification method we
didn't explicitly enumerate (notifications/cancelled, etc.) —
notifications/initialized only worked because it had its own case.
## Smoking gun
agytest's stderr log captured the exact frame:
IN {"jsonrpc":"2.0","method":"notifications/roots/list_changed","params":{}}
agytest's permissive Python parser silently ignored it; our strict
hub responded with an error. The cross-server log diff (agytest
worked, termipod didn't, same input frame) was the load-bearing
clue.
Manual reproduction:
$ echo '{"jsonrpc":"2.0","method":"notifications/roots/list_changed"}' | hub-mcp-bridge
{"jsonrpc":"2.0","error":{"code":-32601,"message":"method not found: ..."}}
(should have been empty body per JSON-RPC spec)
## Fix
Insert a notification gate BEFORE the per-method switch in
server/mcp.go. The standalone daemon (hubmcpserver/run.go) already
handles this correctly via a 'no id → no reply' branch; only the
in-process /mcp/<token> route was buggy.
Lock test: TestMCP_NotificationsGetNoResponse exercises 5
notifications including the host-verified
notifications/roots/list_changed and an unknown-method case (to
confirm the default-path no longer fires for notifications). Each
must return 204 with empty body.
## Why agy's prior diagnosis missed it
agy proposed three theories — stdout corruption, PATH discrepancy,
sandbox restrictions — all wrong. The actual cause was a
wire-level frame agy sent AFTER initialize that hadn't been
considered. Finding it required reading the agytest server's
stderr log (where agy ALSO sent notifications/roots/list_changed)
and comparing what agytest's permissive parser ignored vs what
our strict-handler responded to. Verify-don't-guess + cross-server
log comparison was the load-bearing technique.
## MCP debug arc summary
Four wedges, each a distinct layer of the strict-client wall:
v1.0.649 — hard-coded protocolVersion → agy 2025-11-25 rejected
v1.0.653 — workdir .mcp.json pinned stale token → 401
v1.0.654 — dot-named tool aliases → tools/list batch rejected
v1.0.656 — replied to notifications → client closed transport
Each surfaced only after the prior was fixed — strict clients fail
at the first wall they hit, hiding everything behind it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0 commit comments