Skip to content

fix(client): report a throwing listChanged callback on onerror - #3176

Open
YusefSyed wants to merge 1 commit into
modelcontextprotocol:mainfrom
YusefSyed:fix/list-changed-callback-errors
Open

YusefSyed wants to merge 1 commit into
modelcontextprotocol:mainfrom
YusefSyed:fix/list-changed-callback-errors

Conversation

@YusefSyed

Copy link
Copy Markdown

Fixes #3175.

Motivation and Context

An error thrown by a ClientOptions.listChanged onChanged callback never reaches client.onerror. _setupListChangedHandler starts refresh() from a setTimeout, or calls it without returning it, so the rejection escapes as an unhandled promise rejection. In a plain Node script that ends the process with exit code 1.

With autoRefresh (the default) the callback also runs inside the try around the list fetch, so it is called a second time with its own error as if the refresh had failed. The docs example would log that as Refresh failed:.

This wraps the callback where it is invoked. A throw is reported once on onerror as Uncaught error in <list type> listChanged callback: <error>, following the wording Protocol uses for notification handlers, and the catch around the fetch now only sees fetch errors.

How Has This Been Tested?

  • Two tests added next to the existing listChanged tests in test/integration/test/client/client.test.ts: autoRefresh without debounce, and manual refresh with debounce. Both fail on main and pass here.
  • The script from the linked issue, run with plain node against the built packages: exit code 1 before, clean exit with one client.onerror call after.
  • Locally: pnpm build:all, pnpm -r typecheck, pnpm -r lint, the snippet and skills checks, the dist type smoke test and client conformance pass. Unit and integration suites pass for every SDK package (test/integration: 469 passed). In my sandbox cloudflareWorkers.test.ts, the examples/shared tests and seven e2e scenarios (six stdio, one SSE timeout) cannot run, and they fail the same way on main there. Not run locally: docs:check, server conformance, the examples run, Bun and Deno.

Breaking Changes

None. A callback that throws used to be called twice and then end the process through the unhandled rejection. It is now called once and the error goes to onerror.

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

The same code is on v1.x (src/client/index.ts). I can send a [v1.x] backport after this one if you want it.

Only a synchronous throw is handled. An async callback that rejects stays the caller's promise, since ListChangedCallback returns void.

AI disclosure: the investigation, the change, the tests and this description were written primarily by Claude Code, an AI coding assistant, working at my direction on my machine. I have read the diff. The checks listed above were run locally there.

An error thrown by a listChanged onChanged callback escaped as an
unhandled promise rejection. With autoRefresh the callback was also
called a second time, with its own error passed as a failed refresh.

Catch the error where the callback is invoked and report it on
Client.onerror, so the callback runs once per notification.
@YusefSyed
YusefSyed requested a review from a team as a code owner November 11, 2026 18:03
@changeset-bot

changeset-bot Bot commented Nov 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61699e6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/core-internal Patch

Not sure what this means? Click here to learn what changesets are.

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

@pkg-pr-new

pkg-pr-new Bot commented Nov 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 61699e6

This branch has not been deployed

No deployments
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.

[v2] A listChanged onChanged callback that throws is called twice and ends in an unhandled rejection

1 participant