Skip to content

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

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. mcp-claude commented on Apr 17, 2026

    @mcp-claude

    bug is present on both main and v1.x (needs backport).

    setToolRequestHandlers() / setResourceRequestHandlers() / setPromptRequestHandlers() in mcp.ts each call this.server.registerCapabilities(...) unconditionally on first invocation. Server.registerCapabilities() throws AlreadyConnected once a transport is attached, so any registerTool/registerResource/registerPrompt call made after connect() 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 the registerCapabilities call:

    // 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.ts needs 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 b8886e77 on main):

    $ 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 transport
    

    same output on origin/v1.x (commit bf1e022b).

    code path:

    1. McpServer.registerTool() → this.setToolRequestHandlers() (mcp.ts:833)
    2. setToolRequestHandlers(): _toolHandlersInitialized is false → not returned early (mcp.ts:121-123)
    3. this.server.registerCapabilities({ tools: { ... } }) called (mcp.ts:128)
    4. Server.registerCapabilities(): if (this.transport) → true because connect() already ran → throws SdkError(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 McpServer with tools/resources/prompts capabilities pre-declared in constructor options, call connect(), then assert that registerTool, registerResource, and registerPrompt all succeed without throwing.

  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