Skip to content

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

Open

Description

@YusefSyed

What happened?

When an onChanged callback configured through ClientOptions.listChanged throws, the error is not reported on client.onerror. It becomes an unhandled promise rejection, which ends a Node process with exit code 1.

With autoRefresh on (the default) there is a second problem. The callback runs inside the try that guards the list fetch, so its own exception is caught there and the callback is called again as onChanged(thatError, null), as if the refresh had failed. The docs example logs that argument as Refresh failed:.

Output of the script below on main (919bb8e), built packages, Node 24.18 (paths and stack traces shortened):

$ node repro.mjs
onChanged called with null [ 't' ]
onChanged called with bug in the application callback null
.../repro.mjs:22
                    throw new Error('bug in the application callback');
                          ^
Error: bug in the application callback
    at onChanged (.../repro.mjs:22:27)
    at refresh (.../packages/client/dist/public-B_Q_nc-B.mjs:4029:5)
exit code: 1

$ node repro.mjs manual
onChanged called with null null
.../repro.mjs:22
                    throw new Error('bug in the application callback');
                          ^
Error: bug in the application callback
    at onChanged (.../repro.mjs:22:27)
    at refresh (.../packages/client/dist/public-B_Q_nc-B.mjs:4023:5)
exit code: 1

The cause is in _setupListChangedHandler (packages/client/src/client/client.ts). refresh() is started from a setTimeout when debounced and called without being returned otherwise, so its rejection never reaches the catch in Protocol._onnotification that reports other notification handler errors.

This comes from reading the code and running the script. It is not from a production incident.

What did you expect?

The callback runs once per notification, and an error it throws is reported on client.onerror, the way Uncaught error in notification handler: ... is for other handlers.

Code to reproduce

import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
import { Server } from '@modelcontextprotocol/server';

const autoRefresh = process.argv[2] !== 'manual';

const server = new Server({ name: 's', version: '1' }, { capabilities: { tools: { listChanged: true } } });
server.setRequestHandler('tools/list', async () => ({ tools: [{ name: 't', inputSchema: { type: 'object' } }] }));

const client = new Client(
    { name: 'c', version: '1' },
    {
        listChanged: {
            tools: {
                autoRefresh,
                debounceMs: 0,
                onChanged: (error, tools) => {
                    console.log('onChanged called with', error?.message ?? null, tools?.map(tool => tool.name) ?? null);
                    throw new Error('bug in the application callback');
                }
            }
        }
    }
);
client.onerror = error => console.log('client.onerror:', error.message);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
await server.notification({ method: 'notifications/tools/list_changed' });
await new Promise(resolve => setTimeout(resolve, 200));
await client.close();
console.log('reached the end of the script');

SDK version

2.4.0 (main at 919bb8e). The same code is on v1.x in src/client/index.ts.

Area

Client

Additional context

I have a small fix with two regression tests and a changeset, and will open a PR that references this issue.

AI disclosure: this report and the fix were written primarily by Claude Code, an AI coding assistant, working at my direction on my machine. The output above is from a local run there.

Activity

  1. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    v1Issues / PRs related to v1.x
    on Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions