Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/fix-capabilities-reregistration-after-connect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/sdk': patch
---

Allow dynamic tool/resource/prompt registration after `connect()` when capabilities were pre-supplied at construction, by making `registerCapabilities` idempotent for already-present capability keys.
9 changes: 8 additions & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,14 @@ export class Server<
*/
public registerCapabilities(capabilities: ServerCapabilities): void {
if (this.transport) {
throw new Error('Cannot register capabilities after connecting to transport');
// After connecting, allow if all requested capability keys are already present
// (supports dynamic handler registration when capabilities were pre-supplied at construction).
for (const key in capabilities) {
if (!(key in this._capabilities)) {
throw new Error('Cannot register capabilities after connecting to transport');
}
}
return;
}
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
}
Expand Down
44 changes: 44 additions & 0 deletions test/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,27 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
]);
});

/***
* Test: Dynamic tool registration after connect with pre-supplied capabilities
*/
test('should allow registering tools after connect when capabilities were pre-supplied', async () => {
const mcpServer = 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), mcpServer.connect(serverTransport)]);

// This should NOT throw "Cannot register capabilities after connecting to transport"
mcpServer.tool('dynamic-tool', async () => ({
content: [{ type: 'text', text: 'Dynamic response' }]
}));

const result = await client.request({ method: 'tools/list' }, ListToolsResultSchema);

expect(result.tools).toHaveLength(1);
expect(result.tools[0].name).toBe('dynamic-tool');
});

/***
* Test: Updating Existing Tool
*/
Expand Down Expand Up @@ -2091,6 +2112,29 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
expect(result.resources[0].uri).toBe('test://resource');
});

/***
* Test: Dynamic resource registration after connect with pre-supplied capabilities
*/
test('should allow registering resources after connect when capabilities were pre-supplied', async () => {
const mcpServer = 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), mcpServer.connect(serverTransport)]);

mcpServer.resource('dynamic-resource', 'test://dynamic', async () => ({
contents: [{ uri: 'test://dynamic', text: 'Dynamic content' }]
}));

const result = await client.request({ method: 'resources/list' }, ListResourcesResultSchema);

expect(result.resources).toHaveLength(1);
expect(result.resources[0].name).toBe('dynamic-resource');
});

/***
* Test: Update Resource with URI
*/
Expand Down
Loading