Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/register-after-connect.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Comment thread
felixweinberger marked this conversation as resolved.
}

/**
Expand Down
64 changes: 64 additions & 0 deletions test/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Comment thread
felixweinberger marked this conversation as resolved.
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', () => {
Expand Down
Loading