Repository navigation
CTX7-3074: Close empty MCP subscriptions instead of refusing them - #3330
Conversation
Upgrade @modelcontextprotocol/server and client to 2.2.0 and node to 2.1.0. Server 2.2.0 includes typescript-sdk#2651: a subscriptions/listen stream that honors no notification type ends right after the ack with a `complete` result. Remove the MCP_MAX_SUBSCRIPTIONS cap (0 since #3194). With a cap of 0 the SDK refused every listen with -32603 and reported each refusal through onerror, about 53k "subscription limit reached (0)" log lines a day. Context7 advertises listChanged: false, so every listen now closes at once and no stream holds a gateway connection. 2.3.x is newer than the repo's 7-day minimumReleaseAge.
…ty-mcp-subscriptions
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
SDK 2.2.0 does not complete empty stdio subscriptions, and the central completion behavior lacks regression coverage.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Updates the MCP SDK so empty HTTP subscriptions complete immediately rather than being rejected.
Changes:
- Upgrades MCP dependencies.
- Removes the custom subscription limit and environment setting.
- Adjusts telemetry capacity tests.
| File | Description |
|---|---|
pnpm-lock.yaml |
Locks upgraded MCP dependencies. |
packages/mcp/package.json |
Updates MCP package versions. |
packages/mcp/src/index.ts |
Restores SDK subscription defaults. |
packages/mcp/src/lib/subscriptions.ts |
Removes custom limit handling. |
packages/mcp/test/subscriptions.test.ts |
Removes obsolete limit tests. |
packages/mcp/test/mcp-subscription-telemetry.test.ts |
Adapts capacity tests. |
.env.example |
Removes the subscription setting. |
.changeset/listen-complete-not-refused.md |
Documents the behavioral change. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- SDK 2.2.0 completes an HTTP listen that honors nothing, but its stdio router still keeps such a subscription open, so stdio keeps maxSubscriptions 0 (its refusals never reached onerror). - Add a test that drains an unhonored HTTP listen and checks the empty acknowledgement, the `complete` result and the subscription gauge. The abort and handler-close tests now request toolsListChanged so their streams stay open as those tests assume.
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The dependency update, configuration removal, and regression coverage consistently implement immediate completion for unhonored HTTP subscriptions.
Review effort: Balanced
Findings: None
Resolved since last review (1)
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
ramazanoacar
left a comment
There was a problem hiding this comment.
Approving. I checked the shipped 2.2.0 bundle rather than the description alone, and the core claim holds: createMcpHandler reads capabilities from the per-request server, our createMcpServer declares listChanged: false and subscribe: false explicitly, so honoredSubset is always {} for us and the stream closes right after the ack. Worth keeping in mind that the SDK defaults tools.listChanged to true on tool registration, so that explicit declaration is now load-bearing for the #3194 fix. A short comment next to it would help the next person.
Also checked and fine: stdio keeps the cap at 0 and the stdio router never calls onerror; the SDK's new stdin-EOF close (#2494) and our installProcessShutdown both go through idempotent close paths, so no double close or exit code 1; the new 4 MiB body limit and 100-message batch cap from the node 2.1.0 adapter are far above anything we receive.
One correction for the reviewer notes: the "reject a modern POST without MCP-Protocol-Version" change (#2590) is in server 2.1.0, not 2.3.x, so it ships with this PR. I confirmed the version-header-missing rejection is present in the 2.2.0 bundle and absent in 2.0.0. Clients built with the SDK always send the header, so impact should be nil, but it is reported through onerror, so after deploy it is worth grepping for Rejected inbound request (version-header-missing) to confirm we did not trade one log line for another. Could you update the note so the changeset and PR history stay accurate?
Minor: .env.example cleanup and the node pin at 2.1.0 (2.1.1 is still inside the 7-day minimumReleaseAge) both look right.


Summary
@modelcontextprotocol/serverandclientfrom 2.0.0 to 2.2.0, andnodefrom 2.0.0 to 2.1.0. Server 2.2.0 includes typescript-sdk#2651: asubscriptions/listenstream that honors no notification type ends right after the acknowledgement with aresultType: "complete"result.MCP_MAX_SUBSCRIPTIONScap (0 since CTX7-2768: Disable unused MCP subscriptions #3194) for HTTP; the SDK default cap applies there. Stdio keepsmaxSubscriptions: 0, because the SDK's stdio router still keeps a subscription that honors nothing open. Stdio refusals never reachedonerror.Why
#3194 set the cap to 0 because empty notification streams held ~63k gateway connections. With a cap of 0, the SDK answers every listen with
-32603 "Subscription limit reached"and reports each refusal throughonerror. That is ~53ksubscriptions/listen refused: subscription limit reached (0)lines a day (372k in the 7 days to 2026-10-05).Production advertises
listChanged: falsefor tools, prompts and resources. The TypeScript, Go and Python SDK clients open a listen stream only whenlistChangedis true, so these requests come from client apps that callsubscriptions/listenthemselves. With 2.2.0 they get a correct answer, the stream closes at once, and nothing is logged. No stream stays open, so the gateway problem from #3194 stays solved.Validation
completeresult and the subscription gauge.{}) and thecompleteresult, and the connection closes in ~2 ms. The server logs nothing.toolsListChangedfrom a server that declares it.Reviewer notes
minimumReleaseAge. A later bump to 2.3.x brings one stricter check: a 2026-07-28 POST withoutMCP-Protocol-Versionis rejected.MCP_MAX_SUBSCRIPTIONS, the variable is now ignored and can be removed from the deployment.Closes CTX7-3074