From 2a3ba79c71f6e892bd4ed8c29e8702abb3b665e7 Mon Sep 17 00:00:00 2001 From: XinLei Date: Wed, 11 Mar 2026 22:32:25 -0700 Subject: [PATCH 1/3] fix: allow registering tools/resources/prompts after connect when capabilities pre-declared (#893) When McpServer is constructed with pre-declared capabilities (e.g. `{ capabilities: { tools: { listChanged: true } } }`), calling `registerTool()` / `registerResource()` / `registerPrompt()` after `connect()` used to throw "Cannot register capabilities after connecting to transport" because the first handler registration always called `server.registerCapabilities()` unconditionally. Fix: - Pass capabilities through `withDefaultListChangedCapabilities()` so `listChanged` defaults are applied before `connect()`. - Eagerly initialize the corresponding request handlers in the constructor when capabilities are pre-declared, so the lazy path in `set*RequestHandlers()` is already done before `connect()`. - Guard every `registerCapabilities()` call inside the four `set*RequestHandlers()` / `setCompletionRequestHandler()` methods with `!this.server.transport`, so post-connect invocations skip the now-redundant re-registration. Fixes https://github.com/modelcontextprotocol/typescript-sdk/issues/893 Co-Authored-By: Claude Opus 4.6 --- packages/server/src/server/mcp.ts | 77 ++++++--- .../test893.post-connect-registration.test.ts | 160 ++++++++++++++++++ 2 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 test/integration/test/issues/test893.post-connect-registration.test.ts diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 05136f5b6..7c54a6b92 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -78,7 +78,18 @@ export class McpServer { private _experimental?: { tasks: ExperimentalMcpServerTasks }; constructor(serverInfo: Implementation, options?: ServerOptions) { - this.server = new Server(serverInfo, options); + this.server = new Server(serverInfo, withDefaultListChangedCapabilities(options)); + + const capabilities = this.server.getCapabilities(); + if (capabilities.tools) { + this.setToolRequestHandlers(); + } + if (capabilities.resources) { + this.setResourceRequestHandlers(); + } + if (capabilities.prompts) { + this.setPromptRequestHandlers(); + } } /** @@ -130,11 +141,13 @@ export class McpServer { this.server.assertCanSetRequestHandler('tools/list'); this.server.assertCanSetRequestHandler('tools/call'); - this.server.registerCapabilities({ - tools: { - listChanged: this.server.getCapabilities().tools?.listChanged ?? true - } - }); + if (!this.server.transport) { + this.server.registerCapabilities({ + tools: { + listChanged: this.server.getCapabilities().tools?.listChanged ?? true + } + }); + } this.server.setRequestHandler( 'tools/list', @@ -354,9 +367,11 @@ export class McpServer { this.server.assertCanSetRequestHandler('completion/complete'); - this.server.registerCapabilities({ - completions: {} - }); + if (!this.server.transport) { + this.server.registerCapabilities({ + completions: {} + }); + } this.server.setRequestHandler('completion/complete', async (request): Promise => { switch (request.params.ref.type) { @@ -442,11 +457,13 @@ export class McpServer { this.server.assertCanSetRequestHandler('resources/templates/list'); this.server.assertCanSetRequestHandler('resources/read'); - this.server.registerCapabilities({ - resources: { - listChanged: this.server.getCapabilities().resources?.listChanged ?? true - } - }); + if (!this.server.transport) { + this.server.registerCapabilities({ + resources: { + listChanged: this.server.getCapabilities().resources?.listChanged ?? true + } + }); + } this.server.setRequestHandler('resources/list', async (_request, ctx) => { const resources = Object.entries(this._registeredResources) @@ -522,11 +539,13 @@ export class McpServer { this.server.assertCanSetRequestHandler('prompts/list'); this.server.assertCanSetRequestHandler('prompts/get'); - this.server.registerCapabilities({ - prompts: { - listChanged: this.server.getCapabilities().prompts?.listChanged ?? true - } - }); + if (!this.server.transport) { + this.server.registerCapabilities({ + prompts: { + listChanged: this.server.getCapabilities().prompts?.listChanged ?? true + } + }); + } this.server.setRequestHandler( 'prompts/list', @@ -1006,6 +1025,26 @@ export class McpServer { } } +function withDefaultListChangedCapabilities(options?: ServerOptions): ServerOptions | undefined { + if (!options?.capabilities) { + return options; + } + + const { capabilities } = options; + + return { + ...options, + capabilities: { + ...capabilities, + ...(capabilities.tools ? { tools: { listChanged: capabilities.tools.listChanged ?? true, ...capabilities.tools } } : {}), + ...(capabilities.resources + ? { resources: { listChanged: capabilities.resources.listChanged ?? true, ...capabilities.resources } } + : {}), + ...(capabilities.prompts ? { prompts: { listChanged: capabilities.prompts.listChanged ?? true, ...capabilities.prompts } } : {}) + } + }; +} + /** * A callback to complete one variable within a resource template's URI template. */ diff --git a/test/integration/test/issues/test893.post-connect-registration.test.ts b/test/integration/test/issues/test893.post-connect-registration.test.ts new file mode 100644 index 000000000..54e6f6738 --- /dev/null +++ b/test/integration/test/issues/test893.post-connect-registration.test.ts @@ -0,0 +1,160 @@ +import { Client } from '@modelcontextprotocol/client'; +import { InMemoryTransport } from '@modelcontextprotocol/core'; +import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; +import * as z from 'zod/v4'; + +describe('Issue #893: post-connect registration with pre-declared capabilities', () => { + test('registers a tool after connect when capabilities.tools is pre-declared', async () => { + const server = new McpServer({ name: 'test-server', version: '1.0' }, { capabilities: { tools: { listChanged: true } } }); + const client = new Client({ name: 'test-client', version: '1.0' }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + + expect(() => + server.registerTool('echo', {}, async () => ({ + content: [{ type: 'text', text: 'tool registered after connect' }] + })) + ).not.toThrow(); + + const result = await client.callTool({ name: 'echo' }); + expect(result.content).toEqual([{ type: 'text', text: 'tool registered after connect' }]); + }); + + test('registers a resource after connect when capabilities.resources is pre-declared', async () => { + const server = new McpServer({ name: 'test-server', version: '1.0' }, { capabilities: { resources: { listChanged: true } } }); + const client = new Client({ name: 'test-client', version: '1.0' }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + + expect(() => + server.registerResource('settings', 'test://settings', {}, async () => ({ + contents: [{ uri: 'test://settings', text: 'resource registered after connect' }] + })) + ).not.toThrow(); + + const result = await client.readResource({ uri: 'test://settings' }); + expect(result.contents).toEqual([{ uri: 'test://settings', text: 'resource registered after connect' }]); + }); + + test('registers a prompt after connect when capabilities.prompts is pre-declared', async () => { + const server = new McpServer({ name: 'test-server', version: '1.0' }, { capabilities: { prompts: { listChanged: true } } }); + const client = new Client({ name: 'test-client', version: '1.0' }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + + expect(() => + server.registerPrompt('review', {}, async () => ({ + messages: [ + { + role: 'assistant', + content: { type: 'text', text: 'prompt registered after connect' } + } + ] + })) + ).not.toThrow(); + + const result = await client.request({ + method: 'prompts/get', + params: { name: 'review' } + }); + expect(result.messages).toEqual([ + { + role: 'assistant', + content: { type: 'text', text: 'prompt registered after connect' } + } + ]); + }); + + test('registers a completable prompt after connect when capabilities.prompts is pre-declared', async () => { + const server = new McpServer( + { name: 'test-server', version: '1.0' }, + { capabilities: { prompts: { listChanged: true }, completions: {} } } + ); + const client = new Client({ name: 'test-client', version: '1.0' }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + + expect(() => + server.registerPrompt( + 'review-with-completion', + { + argsSchema: z.object({ + tone: completable(z.string(), () => ['direct', 'formal']) + }) + }, + async ({ tone }) => ({ + messages: [ + { + role: 'assistant', + content: { type: 'text', text: `tone=${tone}` } + } + ] + }) + ) + ).not.toThrow(); + + const result = await client.request({ + method: 'completion/complete', + params: { + ref: { + type: 'ref/prompt', + name: 'review-with-completion' + }, + argument: { + name: 'tone', + value: '' + } + } + }); + + expect(result.completion.values).toEqual(['direct', 'formal']); + expect(result.completion.total).toBe(2); + }); + + test('registers a completable resource template after connect when capabilities.resources is pre-declared', async () => { + const server = new McpServer( + { name: 'test-server', version: '1.0' }, + { capabilities: { resources: { listChanged: true }, completions: {} } } + ); + const client = new Client({ name: 'test-client', version: '1.0' }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + + expect(() => + server.registerResource( + 'repo', + new ResourceTemplate('test://repos/{name}', { + complete: { + name: () => ['alpha', 'beta'] + } + }), + {}, + async () => ({ + contents: [{ uri: 'test://repos/alpha', text: 'resource registered after connect' }] + }) + ) + ).not.toThrow(); + + const result = await client.request({ + method: 'completion/complete', + params: { + ref: { + type: 'ref/resource', + uri: 'test://repos/{name}' + }, + argument: { + name: 'name', + value: '' + } + } + }); + + expect(result.completion.values).toEqual(['alpha', 'beta']); + expect(result.completion.total).toBe(2); + }); +}); From d53b63bf204812d226e3d38d20453ae45f3117e0 Mon Sep 17 00:00:00 2001 From: XinLei Date: Wed, 11 Mar 2026 22:34:19 -0700 Subject: [PATCH 2/3] ci: retrigger CI (infra flake on pnpm/action-setup@v4) From b6fa68f3ffa5711dd5c6f53d5472d9f82e863288 Mon Sep 17 00:00:00 2001 From: XinLei Date: Thu, 12 Mar 2026 01:56:49 -0700 Subject: [PATCH 3/3] fix: correct spread order in withDefaultListChangedCapabilities (#893) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit { listChanged: X ?? true, ...capabilities.tools } puts the default first so the spread overwrites it — explicit undefined defeats the default. Fix to { ...capabilities.tools, listChanged: X ?? true } so the ?? true only applies when the user did not set listChanged. Co-Authored-By: Claude Opus 4.6 --- packages/server/src/server/mcp.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 7c54a6b92..8cb00a78c 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1036,11 +1036,11 @@ function withDefaultListChangedCapabilities(options?: ServerOptions): ServerOpti ...options, capabilities: { ...capabilities, - ...(capabilities.tools ? { tools: { listChanged: capabilities.tools.listChanged ?? true, ...capabilities.tools } } : {}), + ...(capabilities.tools ? { tools: { ...capabilities.tools, listChanged: capabilities.tools.listChanged ?? true } } : {}), ...(capabilities.resources - ? { resources: { listChanged: capabilities.resources.listChanged ?? true, ...capabilities.resources } } + ? { resources: { ...capabilities.resources, listChanged: capabilities.resources.listChanged ?? true } } : {}), - ...(capabilities.prompts ? { prompts: { listChanged: capabilities.prompts.listChanged ?? true, ...capabilities.prompts } } : {}) + ...(capabilities.prompts ? { prompts: { ...capabilities.prompts, listChanged: capabilities.prompts.listChanged ?? true } } : {}) } }; }