Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Most clients expect a server to be running. Start one from [`../server/README.md
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
| Task interactive client | Demonstrates task-based execution + interactive server→client requests. | [`src/simpleTaskInteractiveClient.ts`](src/simpleTaskInteractiveClient.ts) |
| Tool list changed notifications | In-process example showing `notifications/tools/list_changed` end-to-end (no networking). | [`src/toolListChanged.ts`](src/toolListChanged.ts) |

## URL elicitation example (server + client)

Expand Down
2 changes: 2 additions & 0 deletions examples/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
},
"dependencies": {
"@modelcontextprotocol/client": "workspace:^",
"@modelcontextprotocol/core": "workspace:^",
"@modelcontextprotocol/server": "workspace:^",
"ajv": "catalog:runtimeShared",
"open": "^11.0.0",
"zod": "catalog:runtimeShared"
Expand Down
115 changes: 115 additions & 0 deletions examples/client/src/toolListChanged.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Tool list changed notification example.
*
* Demonstrates how a client subscribes to `notifications/tools/list_changed`
* and automatically refreshes its tool list when the server registers a new
* tool after the connection is established.
*
* Uses InMemoryTransport so the example runs fully in-process — no server
* process or network required.
*
* Expected output:
* [server] started with 1 tool: get_weather
* [client] connected. initial tools: [ 'get_weather' ]
* [server] registering new tool: get_forecast
* [client] tool list changed — updated tools: [ 'get_weather', 'get_forecast' ]
*
* Closes #1132
*/

import { Client } from '@modelcontextprotocol/client';
import { InMemoryTransport } from '@modelcontextprotocol/core';
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod/v4';

async function main(): Promise<void> {
// --- Server setup ---------------------------------------------------
const server = new McpServer({ name: 'weather-server', version: '1.0.0' });

server.registerTool(
'get_weather',
{
description: 'Get current weather for a city',
inputSchema: z.object({ city: z.string() })
},
async ({ city }) => ({ content: [{ type: 'text', text: `Sunny in ${city}` }] })
);

console.log('[server] started with 1 tool: get_weather');

// --- Client setup ---------------------------------------------------
// `listChanged.tools.onChanged` fires automatically whenever the server
// sends `notifications/tools/list_changed`. The SDK re-fetches the full
// tool list and passes it to `onChanged`.
//
// By default the client debounces list-changed notifications by 300 ms
// (so rapid back-to-back registrations coalesce into a single refresh).
// Here we set debounceMs: 0 so the callback fires immediately, which
// keeps the example output predictable.
let resolveChanged: () => void;
const changedOnce = new Promise<void>(r => {
resolveChanged = r;
});

const client = new Client(
{ name: 'weather-client', version: '1.0.0' },
{
listChanged: {
tools: {
debounceMs: 0,
onChanged: (_error, tools) => {
if (_error) {
console.error('[client] error refreshing tools:', _error.message);
return;
}
const names = tools?.map(t => t.name) ?? [];
console.log('[client] tool list changed — updated tools:', names);
resolveChanged();
}
}
}
}
);

// --- Connect via in-memory transport --------------------------------
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);

const { tools: initialTools } = await client.listTools();
console.log(
'[client] connected. initial tools:',
initialTools.map(t => t.name)
);

// --- Dynamic tool registration --------------------------------------
// Registering a tool AFTER connect fires sendToolListChanged() inside
// McpServer, which pushes `notifications/tools/list_changed` to the
// client. The `onChanged` callback above runs automatically.
await new Promise<void>(resolve => setTimeout(resolve, 500));

console.log('[server] registering new tool: get_forecast');
server.registerTool(
'get_forecast',
{
description: 'Get a 5-day weather forecast for a city',
inputSchema: z.object({ city: z.string(), days: z.number().int().min(1).max(5).default(3) })
},
async ({ city, days }) => ({
content: [{ type: 'text', text: `${days}-day forecast for ${city}: sunny throughout` }]
})
);

// Wait for the onChanged callback to confirm the notification arrived
// before closing the connection.
await changedOnce;

await client.close();
}

try {
await main();
} catch (error) {
console.error('Error running tool list changed example:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
5 changes: 4 additions & 1 deletion examples/client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
],
"@modelcontextprotocol/eslint-config": ["./node_modules/@modelcontextprotocol/eslint-config/tsconfig.json"],
"@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"],
"@modelcontextprotocol/examples-shared": ["./node_modules/@modelcontextprotocol/examples-shared/src/index.ts"]
"@modelcontextprotocol/examples-shared": ["./node_modules/@modelcontextprotocol/examples-shared/src/index.ts"],
"@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"],
"@modelcontextprotocol/server/stdio": ["./node_modules/@modelcontextprotocol/server/src/stdio.ts"],
"@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"]
}
}
}
19 changes: 11 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading