From b2e21ba1e1ad6c8786fa0f297de052640ba4a719 Mon Sep 17 00:00:00 2001 From: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:48:43 +0000 Subject: [PATCH 1/3] fix: allow registering after connect() when the capability was declared McpServer installed the tools, resources and prompts handlers on the first registration, and installing them registers the capability, which throws once a transport is connected. So a server that declared a capability in its options and registered its first tool after connect() got 'Cannot register capabilities after connecting to transport'. A capability declared in the constructor options now gets its handlers in the constructor, as on main (#2269). Registering after connect() then works, and a declared capability with nothing registered answers its list request with an empty list instead of 'Method not found'. A capability declared as {} is advertised with listChanged: true. Registering after connect() without having declared the capability still throws. --- src/server/mcp.ts | 11 +++++++ test/server/mcp.test.ts | 64 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 9048057098..aa21af7bbf 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -132,6 +132,17 @@ export class McpServer { constructor(serverInfo: Implementation, options?: McpServerOptions) { this.server = new Server(serverInfo, options); this._maxToolInputElements = resolveMaxToolInputElements(options?.maxToolInputElements); + + // A capability declared in the options gets its handlers now, so registering after connect() works. + if (options?.capabilities?.tools) { + this.setToolRequestHandlers(); + } + if (options?.capabilities?.resources) { + this.setResourceRequestHandlers(); + } + if (options?.capabilities?.prompts) { + this.setPromptRequestHandlers(); + } } /** diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index da6783c670..9a8db72117 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -252,6 +252,70 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(capabilities?.extensions).toBeDefined(); expect(capabilities?.extensions?.['io.modelcontextprotocol/test-extension']).toEqual({ streaming: true }); }); + + /*** + * Test: Capabilities Declared in the Constructor + */ + test('should allow registering after connect when the capability was declared', async () => { + const mcpServer = new McpServer( + { name: 'test server', version: '1.0' }, + { capabilities: { tools: { listChanged: true }, resources: { listChanged: true }, prompts: { listChanged: true } } } + ); + const client = new Client({ name: 'test client', version: '1.0' }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + mcpServer.registerTool('late-tool', { inputSchema: { name: z.string() } }, async ({ name }) => ({ + content: [{ type: 'text', text: `Hello, ${name}` }] + })); + mcpServer.registerResource('late-resource', 'test://late', {}, async () => ({ + contents: [{ uri: 'test://late', text: 'late' }] + })); + mcpServer.registerPrompt('late-prompt', {}, async () => ({ + messages: [{ role: 'user', content: { type: 'text', text: 'late' } }] + })); + + const tools = await client.request({ method: 'tools/list' }, ListToolsResultSchema); + expect(tools.tools.map(tool => tool.name)).toEqual(['late-tool']); + const resources = await client.request({ method: 'resources/list' }, ListResourcesResultSchema); + expect(resources.resources.map(resource => resource.name)).toEqual(['late-resource']); + const prompts = await client.request({ method: 'prompts/list' }, ListPromptsResultSchema); + expect(prompts.prompts.map(prompt => prompt.name)).toEqual(['late-prompt']); + + const result = await client.request( + { method: 'tools/call', params: { name: 'late-tool', arguments: { name: 'world' } } }, + CallToolResultSchema + ); + expect(result.content).toEqual([{ type: 'text', text: 'Hello, world' }]); + }); + + test('should answer list requests with empty lists when a capability was declared and nothing is registered', async () => { + const mcpServer = new McpServer( + { name: 'test server', version: '1.0' }, + { capabilities: { tools: {}, resources: {}, prompts: {} } } + ); + const client = new Client({ name: 'test client', version: '1.0' }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + expect((await client.request({ method: 'tools/list' }, ListToolsResultSchema)).tools).toEqual([]); + expect((await client.request({ method: 'resources/list' }, ListResourcesResultSchema)).resources).toEqual([]); + expect((await client.request({ method: 'prompts/list' }, ListPromptsResultSchema)).prompts).toEqual([]); + }); + + test('should still refuse registering after connect when the capability was not declared', async () => { + const mcpServer = new McpServer({ name: 'test server', version: '1.0' }); + const client = new Client({ name: 'test client', version: '1.0' }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + expect(() => mcpServer.registerTool('late-tool', {}, async () => ({ content: [] }))).toThrow( + 'Cannot register capabilities after connecting to transport' + ); + }); }); describe('ResourceTemplate', () => { From 3e5829ebb88c370024e4120a5c2734fbf55461f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 12:54:33 +0000 Subject: [PATCH 2/3] chore: add changeset Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012VRbFCp41otcScXE1YY3es --- .changeset/register-after-connect.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/register-after-connect.md diff --git a/.changeset/register-after-connect.md b/.changeset/register-after-connect.md new file mode 100644 index 0000000000..8432c76954 --- /dev/null +++ b/.changeset/register-after-connect.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/sdk': patch +--- + +`McpServer` now accepts `registerTool()`, `registerResource()` and `registerPrompt()` after `connect()` when the capability was declared in the constructor options; before, the first registration after connecting threw `Cannot register capabilities after connecting to transport`. +A declared capability with nothing registered now answers its list request with an empty list instead of `Method not found`, and a capability declared as `{}` is advertised with `listChanged: true`. From 20cb3c5b8f21c7873432c5b863e641f90d66ed14 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 13:07:22 +0000 Subject: [PATCH 3/3] chore: correct the changeset wording Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012VRbFCp41otcScXE1YY3es --- .changeset/register-after-connect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/register-after-connect.md b/.changeset/register-after-connect.md index 8432c76954..447005a879 100644 --- a/.changeset/register-after-connect.md +++ b/.changeset/register-after-connect.md @@ -3,4 +3,4 @@ --- `McpServer` now accepts `registerTool()`, `registerResource()` and `registerPrompt()` after `connect()` when the capability was declared in the constructor options; before, the first registration after connecting threw `Cannot register capabilities after connecting to transport`. -A declared capability with nothing registered now answers its list request with an empty list instead of `Method not found`, and a capability declared as `{}` is advertised with `listChanged: true`. +A declared capability with nothing registered now answers its list request with an empty list instead of `Method not found`, and is advertised with `listChanged: true`, as it already was once anything had been registered.