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
On a session negotiated at 2026-07-28, the notifications/cancelled the client emits when a call's context is canceled is missing the SEP-2575 per-request _meta envelope. The two cancellation-notify sites (cancelCall and the async-notify branch of call, both in mcp/transport.go) build CancelledParams directly and are the only client sends that skip injectRequestMeta — the transport-layer counterpart of #1130/#1116's Ping/SetLoggingLevel omissions, which the earlier sweeps missed because it is not a ClientSession method.
A conformant server — this SDK's own stateless handler included — rejects the notification with -32602 "missing or invalid _meta field \"io.modelcontextprotocol/protocolVersion\"" over HTTP 400. Because the rejection responds to a notification, its JSON-RPC error body has no id, so checkResponse's decode-and-wrap-in-ErrRejected path (#1118) cannot classify it as a per-call rejection: the client treats it as a fatal write error and permanently fails the whole connection (c.fail).
Practical consequences on a stateless 2026-07-28 session:
ClientSession.Unsubscribe poisons the session. Unsubscribe cancels the per-URI subscriptions/listen call, which fires the doomed notification. Every subsequent awaited call returns connection closed: ... client is closing: sending "notifications/cancelled": Bad Request, and fire-and-forget sends — a re-Subscribe included — vanish with no error surfacing anywhere. An Unsubscribe + Subscribe recovery/rotation cycle therefore kills the session it is trying to refresh.
Canceling any in-flight request does the same. A timed-out or abandoned tool call takes the whole session with it shortly after.
Reproduced on v1.7.0 and on current main (21c18c6) with identical output.
To Reproduce
Self-contained; client and server are both this SDK. The middleware only logs what crosses the wire.
negotiated=2026-07-28
subscribe: <nil>
unsubscribe: <nil>
[wire] notifications/cancelled -> 400 meta_present=false resp={"jsonrpc":"2.0","error":{"code":-32602,"message":"missing or invalid _meta field \"io.modelcontextprotocol/protocolVersion\""}}
listTools after unsubscribe: POISONED: connection closed: calling "tools/list": client is closing: sending "notifications/cancelled": Bad Request
Expected behavior
The cancellation notification carries the same SEP-2575 _meta envelope as every other client send on a 2026-07-28 session, and the server accepts it (202 with no body). Independently, a rejected best-effort cancellation notification should not tear down the connection it was trying to tidy up.
Found while exercising subscription rotation (Unsubscribe + re-Subscribe) against a stateless server: every rotation killed the session.
The escalation half — a rejection whose JSON-RPC error body has no id (as any response to a notification) bypassing mcp: fix error propagation to prevent session closing #1118's ErrRejected classification in checkResponse — is arguably its own defect and would have contained the blast radius here; happy to file it separately if useful.
Describe the bug
On a session negotiated at
2026-07-28, thenotifications/cancelledthe client emits when a call's context is canceled is missing the SEP-2575 per-request_metaenvelope. The two cancellation-notify sites (cancelCalland the async-notify branch ofcall, both inmcp/transport.go) buildCancelledParamsdirectly and are the only client sends that skipinjectRequestMeta— the transport-layer counterpart of #1130/#1116'sPing/SetLoggingLevelomissions, which the earlier sweeps missed because it is not aClientSessionmethod.A conformant server — this SDK's own stateless handler included — rejects the notification with
-32602 "missing or invalid _meta field \"io.modelcontextprotocol/protocolVersion\""over HTTP 400. Because the rejection responds to a notification, its JSON-RPC error body has noid, socheckResponse's decode-and-wrap-in-ErrRejectedpath (#1118) cannot classify it as a per-call rejection: the client treats it as a fatal write error and permanently fails the whole connection (c.fail).Practical consequences on a stateless
2026-07-28session:ClientSession.Unsubscribepoisons the session. Unsubscribe cancels the per-URIsubscriptions/listencall, which fires the doomed notification. Every subsequent awaited call returnsconnection closed: ... client is closing: sending "notifications/cancelled": Bad Request, and fire-and-forget sends — a re-Subscribeincluded — vanish with no error surfacing anywhere. An Unsubscribe + Subscribe recovery/rotation cycle therefore kills the session it is trying to refresh.Reproduced on v1.7.0 and on current
main(21c18c6) with identical output.To Reproduce
Self-contained; client and server are both this SDK. The middleware only logs what crosses the wire.
Output on v1.7.0 and on
main(identical):Expected behavior
The cancellation notification carries the same SEP-2575
_metaenvelope as every other client send on a2026-07-28session, and the server accepts it (202 with no body). Independently, a rejected best-effort cancellation notification should not tear down the connection it was trying to tidy up.Additional context
_metaenvelope into the two cancellation-notify sites; with it the cancellation is accepted (202) and the session stays usable.id(as any response to a notification) bypassing mcp: fix error propagation to prevent session closing #1118'sErrRejectedclassification incheckResponse— is arguably its own defect and would have contained the blast radius here; happy to file it separately if useful.