Skip to content

McpServer re-registers capabilities after connect, blocking dynamic registration even when capabilities were supplied at construction #893

Closed

Description

@inverted-capital

Summary

When creating McpServer with ServerOptions.capabilities that include resources and tools, calling registerResource or registerTool after connect() fails with “Cannot register capabilities after connecting to transport”. The high-level server re-calls server.registerCapabilities(...) the first time handlers are installed, even post-connect.

Steps to Reproduce

  1. Create McpServer with capabilities:
const capabilities = {
  resources: { subscribe: true, listChanged: true },
  tools: { listChanged: true },
};
const server = new McpServer({ name: "demo", version: "1.0.0" }, { capabilities });
  1. await server.connect(transport);
  2. After connect, call server.registerResource(...) (or server.registerTool(...))
  3. Observe HTTP 500 with error “Cannot register capabilities after connecting to transport”

Expected Behavior

  • If capabilities were provided at construction, registering resources/tools/prompts after connect should work. The server should not attempt to re-register capabilities post-connect.

Actual Behavior

  • First registration after connect triggers handler initialization in McpServer, which unconditionally calls server.registerCapabilities(...). Since Server.registerCapabilities forbids post-connect, it throws.

Notes

  • Calls occur in typescript-sdk/src/server/mcp.ts within:
    • setResourceRequestHandlers() → this.server.registerCapabilities({ resources: { listChanged: true } })
    • setToolRequestHandlers() → this.server.registerCapabilities({ tools: { listChanged: true } })
    • setPromptRequestHandlers() → this.server.registerCapabilities({ prompts: { listChanged: true } })
  • Guard that throws is in typescript-sdk/src/server/index.ts registerCapabilities().

Proposed Fix

  • Make this.server.registerCapabilities(...) idempotent if capabilities are already registered.

Environment

  • @modelcontextprotocol/sdk 1.17.3
  • Transport: Streamable HTTP (client error surfaced as HTTP 500)

Activity

  1. Steve-Mcl commented on Sep 7, 2025

    @Steve-Mcl

    @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.

  2. added
    ready for workEnough information for someone to start working on
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Sep 29, 2025
  3. sstklen commented on Feb 24, 2026

    @sstklen

    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.

  4. ghost added
    fix proposedBot has a verified fix diff in the comment
    on Apr 17, 2026
  5. felixweinberger commented on Oct 5, 2026

    @felixweinberger
    Contributor

    Fixed in v2 since 2.0.0 (#2269). On v1 it's in the next patch release (#2958). Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingfix proposedBot has a verified fix diff in the commentready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions