Repository navigation
McpServer re-registers capabilities after connect, blocking dynamic registration even when capabilities were supplied at construction #893
Description
Activity
@inverted-capital I faced the same issue when working on a project that needed to create tools/resources/prompts at runtime.
The workaround I used was to register dummies (1 of each type of tool/resource/prompt) before a transport is established. This seemed to enable the capability for each type in the SDK permitting later things to be registered even after connection. NOTE: They can even be removed after connection and things still work - though since they are disabled, it is fine to keep them around until other tools/resources/prompts are registered.It is not ideal but its working well for me.
Reacted by Lurchfresser- addedready for workEnough information for someone to start working onEnough information for someone to start working onP2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable feature
on Sep 29, 2025 Hey! I ran into a similar pattern in our bug knowledge base and thought this might help.
What's happening: McpServer lazily calls server.registerCapabilities() on first setRequestHandler() call for each type (tools/resources/prompts). If connect() already happened, registerCapabilities() throws. Even if capabilities were declared in constructor, the internal Server only receives them when a handler is first installed — which may be post-connect.
What worked for us:
In McpServer.connect(), before calling this._server.connect(transport), eagerly install the request handlers (and thus trigger registerCapabilities) for any capability types declared in constructor options. Alternatively, patch Server.registerCapabilities() to no-op for capabilities already registered pre-connect. Workaround: register one dummy tool/resource/prompt before connect() to force early capability registration, then remove them post-connect.
// Workaround — register dummies before connect to force capability registration const server = new McpServer({ name: "demo", version: "1.0.0" }, { capabilities }); // Force early capability registration server.tool("__init", "dummy", async () => ({ content: [] })); server.resource("__init", "dummy://init", async () => ({ contents: [] })); server.prompt("__init", "dummy", async () => ({ messages: [] })); await server.connect(transport); // Safe to remove dummies after connect server.removeTool("__init"); server.removeResource("__init"); server.removePrompt("__init"); // Now dynamic registration works server.tool("real-tool", "desc", async () => ({ content: [{ type: "text", text: "ok" }] }));
Hope this helps! Let me know if it doesn't match your case — happy to dig deeper. 🦞
Disclosure: This analysis is from Confucius Debug, an AI-powered community KB for agent bugs. Please verify before applying.
🦞 Confucius Debug — community knowledge base for AI agent bugs. Free to search via MCP.
bug is present on both
mainandv1.x(needs backport).setToolRequestHandlers()/setResourceRequestHandlers()/setPromptRequestHandlers()inmcp.tseach callthis.server.registerCapabilities(...)unconditionally on first invocation.Server.registerCapabilities()throwsAlreadyConnectedonce a transport is attached, so anyregisterTool/registerResource/registerPromptcall made afterconnect()fails — even when the capability was declared at construction time.workaround: register one dummy tool/resource/prompt before
connect()to force handler initialization, then remove it afterwards (as noted in the comments).fix path: in each
set*RequestHandlers(), guard theregisterCapabilitiescall:// setToolRequestHandlers — mcp.ts:128 if (!this.server.getCapabilities().tools) { this.server.registerCapabilities({ tools: { listChanged: true } }); } // same pattern for resources (line 437) and prompts (line 517)
this preserves the existing throw when the capability was not pre-declared and the user attempts post-connect registration (still unsupported), while letting pre-declared capabilities skip the redundant call. only
mcp.tsneeds changing.repro + output + code path
repro script (
repro.ts, run from repo root):import { McpServer } from "./packages/server/src/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/core"; async function main() { const capabilities = { resources: { subscribe: true, listChanged: true }, tools: { listChanged: true }, prompts: { listChanged: true }, }; const server = new McpServer( { name: "test-server", version: "1.0.0" }, { capabilities } ); const [_clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await server.connect(serverTransport); console.log("Connected OK"); try { server.registerTool("my-tool", { description: "a test tool" }, async () => ({ content: [{ type: "text" as const, text: "hello" }], })); console.log("registerTool after connect: OK"); } catch (e: unknown) { console.error("registerTool after connect FAILED:", (e as Error).message); } try { server.registerResource("my-resource", "resource://test", {}, async () => ({ contents: [{ uri: "resource://test", text: "hello", mimeType: "text/plain" }], })); console.log("registerResource after connect: OK"); } catch (e: unknown) { console.error("registerResource after connect FAILED:", (e as Error).message); } try { server.registerPrompt("my-prompt", { description: "a test prompt" }, async () => ({ messages: [{ role: "user" as const, content: { type: "text" as const, text: "hi" } }], })); console.log("registerPrompt after connect: OK"); } catch (e: unknown) { console.error("registerPrompt after connect FAILED:", (e as Error).message); } await server.close(); } main().catch(console.error);
command + output (commit
b8886e77onmain):$ npx tsx --tsconfig packages/server/tsconfig.json repro.ts Connected OK registerTool after connect FAILED: Cannot register capabilities after connecting to transport registerResource after connect FAILED: Cannot register capabilities after connecting to transport registerPrompt after connect FAILED: Cannot register capabilities after connecting to transportsame output on
origin/v1.x(commitbf1e022b).code path:
McpServer.registerTool()→this.setToolRequestHandlers()(mcp.ts:833)setToolRequestHandlers():_toolHandlersInitializedisfalse→ not returned early (mcp.ts:121-123)this.server.registerCapabilities({ tools: { ... } })called (mcp.ts:128)Server.registerCapabilities():if (this.transport)→truebecauseconnect()already ran → throwsSdkError(AlreadyConnected)(server.ts:212-213)
suggested fix
// packages/server/src/server/mcp.ts - this.server.registerCapabilities({ - tools: { - listChanged: this.server.getCapabilities().tools?.listChanged ?? true - } - }); + if (!this.server.getCapabilities().tools) { + this.server.registerCapabilities({ + tools: { + listChanged: true + } + }); + } // same pattern for resources (~line 437) and prompts (~line 518)
test to verify: construct
McpServerwithtools/resources/promptscapabilities pre-declared in constructor options, callconnect(), then assert thatregisterTool,registerResource, andregisterPromptall succeed without throwing.- addedfix proposedBot has a verified fix diff in the commentBot has a verified fix diff in the comment
on Apr 17, 2026
Summary
When creating
McpServerwithServerOptions.capabilitiesthat includeresourcesandtools, callingregisterResourceorregisterToolafterconnect()fails with “Cannot register capabilities after connecting to transport”. The high-level server re-callsserver.registerCapabilities(...)the first time handlers are installed, even post-connect.Steps to Reproduce
McpServerwith capabilities:await server.connect(transport);server.registerResource(...)(orserver.registerTool(...))Expected Behavior
Actual Behavior
McpServer, which unconditionally callsserver.registerCapabilities(...). SinceServer.registerCapabilitiesforbids post-connect, it throws.Notes
typescript-sdk/src/server/mcp.tswithin:setResourceRequestHandlers()→this.server.registerCapabilities({ resources: { listChanged: true } })setToolRequestHandlers()→this.server.registerCapabilities({ tools: { listChanged: true } })setPromptRequestHandlers()→this.server.registerCapabilities({ prompts: { listChanged: true } })typescript-sdk/src/server/index.tsregisterCapabilities().Proposed Fix
this.server.registerCapabilities(...)idempotent if capabilities are already registered.Environment