Skip to content

Answer subscriptions/listen without building the caller's tool surface - #1074

Merged
helpcodeai merged 2 commits into
mainfrom
fix/light-listen
Oct 14, 2026
Merged

helpcodeai merged 2 commits into
mainfrom
fix/light-listen

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Summary

Claude clients on MCP 2026-07-28 open subscriptions/listen streams and keep them for about five minutes. On Cloud on 13 Oct there were about 240 open at once, roughly 50 new ones a minute, 89% on the shared /mcp and the rest on /mcp/<serverId>.

For a listen the SDK only reads the server's capabilities and name, closes it, and serves the stream itself. We still did the full work every time:

  • shared /mcp: attachVisibleTools filtered the whole tool registry (~50k tools) and resolved the grant and the OAuth client, for nothing;
  • /mcp/<serverId>: the full tool surface was built before the method was known and stayed referenced by the factory closure for the stream's whole life, so about 200 surfaces sat in memory unused.

This came up while looking at today's 12:16 UTC backend crash (V8 "Zone Allocation failed" with the heap at a third of its cap). It is not proven to be the cause; it removes wasted memory and CPU either way. #1072 adds a crash report for the next one.

Changes

  • isListenRequest(body): a single JSON-RPC message with method subscriptions/listen.
  • listenOnlyServer(name, version, capabilities): an McpServer that only declares capabilities.
  • Shared /mcp (fixed mode, identified caller, same condition as servesSharedToolset): a listen is answered before attachVisibleTools, declaring tools.listChanged, as the full shared server does.
  • /mcp/<serverId>: a listen is answered after the inactive-server and membership checks and before the connectors and tools are loaded, declaring tools and resources listChanged. The stateful mode (MCP_STATEFUL_SESSIONS) is unchanged.

Nothing is ever published on these streams: each request gets its own event bus, so the declared capabilities only decide what the acknowledgement lists and whether the stream stays open. Checked against the real SDK: with these capabilities the client gets the same notifications/subscriptions/acknowledged and an open stream; with none the stream would close at once.

Cloud and self-hosted behave the same; on self-hosted the shared /mcp path only applies in fixed mode (MCP_SHARED_ENDPOINT_TOOLS=fixed).

Type

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance improvement
  • Other (describe below)

Testing

  • Added/updated unit tests
  • Tested manually (describe steps below)
  • No tests needed (explain why)

In packages/backend:

  • npx jest src/mcp-server: 22 suites, 353 tests passed. New: listen on shared /mcp touches neither the registry, the grant nor the OAuth client; listen on /mcp/<id> still refuses a non-member with 403, answers a member without loading connectors, content or tools; other methods keep the full path; isListenRequest cases.
  • Without the controller change the new per-server test fails and the shared spec does not compile.
  • npx tsc --noEmit -p tsconfig.json: clean. eslint on the changed files: clean.
  • Manual probe through createMcpHandler with a 2026-07-28 listen: acknowledgement lists toolsListChanged (and resourcesListChanged for the server form), stream stays open.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable)
  • All existing tests pass (npm test)
  • This PR has a descriptive title

Related Issues

Follows #1072.

helpcodeai and others added 2 commits October 13, 2026 14:58
Claude clients on MCP 2026-07-28 keep subscriptions/listen streams open for about five minutes; on 13 Oct about 200 were open at once, ~50 new ones a minute. The SDK reads only the server's capabilities and name for such a stream, yet every listen filtered the whole tool registry on the shared /mcp, and on a server's own URL built its full tool surface and kept it referenced for the stream's life. Listen is now answered from a server that only declares the same list-changed capabilities, after the same auth and membership checks. Nothing is ever published on these streams (each request has its own event bus), so clients see the same acknowledgement and an open stream as before.
@helpcodeai
helpcodeai enabled auto-merge (squash) October 14, 2026 07:58
@helpcodeai
helpcodeai merged commit d48f50e into main Oct 14, 2026
14 checks passed
@helpcodeai
helpcodeai deleted the fix/light-listen branch October 14, 2026 08:05
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant