Skip to content

CTX7-3074: Close empty MCP subscriptions instead of refusing them - #3330

Merged
enesgules merged 4 commits into
masterfrom
CTX7-3074/close-empty-mcp-subscriptions
Oct 7, 2026
Merged

enesgules merged 4 commits into
masterfrom
CTX7-3074/close-empty-mcp-subscriptions

Conversation

@enesgules

@enesgules enesgules commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Upgrade @modelcontextprotocol/server and client from 2.0.0 to 2.2.0, and node from 2.0.0 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 acknowledgement with a resultType: "complete" result.
  • Remove the MCP_MAX_SUBSCRIPTIONS cap (0 since CTX7-2768: Disable unused MCP subscriptions #3194) for HTTP; the SDK default cap applies there. Stdio keeps maxSubscriptions: 0, because the SDK's stdio router still keeps a subscription that honors nothing open. Stdio refusals never reached onerror.

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 through onerror. That is ~53k subscriptions/listen refused: subscription limit reached (0) lines a day (372k in the 7 days to 2026-10-05).

Production advertises listChanged: false for tools, prompts and resources. The TypeScript, Go and Python SDK clients open a listen stream only when listChanged is true, so these requests come from client apps that call subscriptions/listen themselves. 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

  • All 119 MCP tests pass; build and type checking pass. A new test drains an unhonored HTTP listen and checks the empty acknowledgement, the complete result and the subscription gauge.
  • Local server: three listen requests each get the ack (honored {}) and the complete result, and the connection closes in ~2 ms. The server logs nothing.
  • The telemetry tests that need an open stream (abort, handler close, capacity) now request toolsListChanged from a server that declares it.

Reviewer notes

  • 2.3.x is newer than the repo's 7-day minimumReleaseAge. A later bump to 2.3.x brings one stricter check: a 2026-07-28 POST without MCP-Protocol-Version is rejected.
  • If production sets MCP_MAX_SUBSCRIPTIONS, the variable is now ignored and can be removed from the deployment.

Closes CTX7-3074

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.
@linear-code

linear-code Bot commented Oct 6, 2026

Copy link
Copy Markdown

CTX7-3074

Copilot AI 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.

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 Medium severity · 1 Low severity

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.

Comment thread packages/mcp/src/index.ts
Comment thread packages/mcp/test/mcp-subscription-telemetry.test.ts
- 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.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

The release note incorrectly implies the new behavior also applies to stdio subscriptions.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (2)
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread .changeset/listen-complete-not-refused.md Outdated

Copilot AI 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.

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 ramazanoacar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@enesgules
enesgules merged commit afdd129 into master Oct 7, 2026
3 checks passed
@enesgules
enesgules deleted the CTX7-3074/close-empty-mcp-subscriptions branch October 7, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants