diff --git a/.changeset/register-after-connect.md b/.changeset/register-after-connect.md new file mode 100644 index 0000000000..447005a879 --- /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 is advertised with `listChanged: true`, as it already was once anything had been registered. 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', () => {