Skip to content

fix(server): stop createMcpHandler chaining onclose on reused instances - #2787

Open
voidstackloop wants to merge 1 commit into
modelcontextprotocol:mainfrom
voidstackloop:create-mcp-handler-onclose-leak
Open

fix(server): stop createMcpHandler chaining onclose on reused instances#2787
voidstackloop wants to merge 1 commit into
modelcontextprotocol:mainfrom
voidstackloop:create-mcp-handler-onclose-leak

Conversation

@voidstackloop

@voidstackloop voidstackloop commented Sep 11, 2026

Copy link
Copy Markdown

createMcpHandler wraps server.onclose on every request to track the instance in inflight until its exchange tears down:

const previousOnClose = server.onclose;
inflight.add(server);
server.onclose = () => {
    inflight.delete(server);
    previousOnClose?.();
};

If the factory returns the same Server/McpServer instance for every session (a valid pattern), this installs a new wrapper on every request instead of once per instance, chaining an unbounded closure onto the previous onclose. The chain grows for the process's lifetime and, when it eventually runs (session cleanup under load, or handler.close()), recurses one stack frame per accumulated wrapper — observed overflowing around 19-25k accumulated sessions with RangeError: Maximum call stack size exceeded, surfacing as an uncaught async error after close() has already resolved.

Track which instances already have the wrapper installed in a WeakSet and only install once per instance, keeping the chain length at 1 regardless of how many requests reuse it. The normal case (a fresh instance per request, the common factory shape) is unaffected.

Added a regression test that reuses one McpServer across five requests and asserts onclose is only ever assigned once.

Closes #2607

Motivation and Context

How Has This Been Tested?

Breaking Changes

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

`createMcpHandler` wraps `server.onclose` on every request to track the
instance in `inflight` until its exchange tears down:

    const previousOnClose = server.onclose;
    inflight.add(server);
    server.onclose = () => {
        inflight.delete(server);
        previousOnClose?.();
    };

If the factory returns the same `Server`/`McpServer` instance for every
session (a valid pattern), this installs a new wrapper on every request
instead of once per instance, chaining an unbounded closure onto the
previous `onclose`. The chain grows for the process's lifetime and,
when it eventually runs (session cleanup under load, or
`handler.close()`), recurses one stack frame per accumulated wrapper —
observed overflowing around 19-25k accumulated sessions with
`RangeError: Maximum call stack size exceeded`, surfacing as an
uncaught async error after `close()` has already resolved.

Track which instances already have the wrapper installed in a
`WeakSet` and only install once per instance, keeping the chain length
at 1 regardless of how many requests reuse it. The normal case (a
fresh instance per request, the common factory shape) is unaffected.

Added a regression test that reuses one `McpServer` across five
requests and asserts `onclose` is only ever assigned once.

Closes modelcontextprotocol#2607

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@voidstackloop
voidstackloop requested a review from a team as a code owner September 11, 2026 12:19
@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 64c38fb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2787

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2787

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2787

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2787

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2787

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2787

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2787

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2787

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2787

commit: 64c38fb

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.

createMcpHandler: reused McpServer instance grows an unbounded onclose chain — memory leak, then uncatchable RangeError after ~20k requests

1 participant