diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 05136f5b6..8cb00a78c 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: { ...capabilities.tools, listChanged: capabilities.tools.listChanged ?? true } } : {}), + ...(capabilities.resources + ? { resources: { ...capabilities.resources, listChanged: capabilities.resources.listChanged ?? true } } + : {}), + ...(capabilities.prompts ? { prompts: { ...capabilities.prompts, listChanged: capabilities.prompts.listChanged ?? true } } : {}) + } + }; +} + /** * 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); + }); +});