Skip to content

MCPServer unconditionally declares prompts, resources, tools capabilities on initialize #2473

Open

Description

@0717376

Pre-flight

  • Using the latest MCP Python SDK (v1.27.0) and also verified against main @ 3d7b311
  • Searched existing issues and PRs — no duplicate found

Description

Reproduced against main @ 3d7b311 (2026-04-14).

Per schema/2025-11-25/schema.ts L388-L431, each entry in ServerCapabilities is documented as "Present if the server offers any [prompt templates | resources | tools]". lifecycle.mdx also requires both peers to only use capabilities that were successfully negotiated.

Current behavior is inconsistent with the documented schema semantics: MCPServer announces prompts, resources, and tools capabilities on every initialize response, regardless of whether any prompt / resource / tool has actually been registered. A server that only exposes one tool still advertises prompts and resources it doesn't have, and an entirely empty server advertises all three.

Root cause (current main):

  1. src/mcp/server/mcpserver/server.py#L178-L184 — MCPServer.__init__ unconditionally wires on_list_tools=self._handle_list_tools, on_list_resources=..., on_list_prompts=... into the lowlevel Server(...).
  2. src/mcp/server/lowlevel/server.py#L205-L224 — Server.__init__ unconditionally populates self._request_handlers with "tools/list", "resources/list", "prompts/list" entries.
  3. src/mcp/server/lowlevel/server.py#L283-L328 — get_capabilities() derives capability entries from key presence in _request_handlers, so all three are always included.

This behavior was carried over verbatim from FastMCP._setup_handlers() in the #1951 rename — the symptom pre-dates the refactor.

The maintainer TODO at lowlevel/server.py#L249-L253 (Rethink capabilities API ... Consider deriving capabilities entirely from server state) points to the same area — happy to align a fix with whatever direction you have in mind there.

Expected: capability entries should only appear when the corresponding primitive is actually offered (registered via decorator or add_*()).

Example Code

# empty_server.py
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("empty")
if __name__ == "__main__":
    mcp.run()
# one_tool_server.py
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("hello")

@mcp.tool()
def echo(text: str) -> str:
    return text

if __name__ == "__main__":
    mcp.run()
# probe.py — sends initialize over stdio and prints the advertised capabilities
import json, subprocess, sys
for path in ["empty_server.py", "one_tool_server.py"]:
    proc = subprocess.Popen([sys.executable, path],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    req = {"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-11-25","capabilities":{},
        "clientInfo":{"name":"probe","version":"0"}}}
    proc.stdin.write((json.dumps(req)+"\n").encode()); proc.stdin.flush()
    print(path)
    print(json.dumps(json.loads(proc.stdout.readline())["result"]["capabilities"], indent=2))
    proc.terminate()

Actual output (both servers, identical):

{
  "experimental": {},
  "prompts":   {"listChanged": false},
  "resources": {"subscribe": false, "listChanged": false},
  "tools":     {"listChanged": false}
}

Expected:

  • empty_server.py → no prompts, resources, or tools entries.
  • one_tool_server.py → tools present, prompts and resources absent.

Possible fixes (open to maintainer preference)

A. Filter capabilities in MCPServer based on manager state.
Post-process the ServerCapabilities returned by the lowlevel Server — omit tools / resources / prompts when the respective manager is empty. Minimal diff, keeps the decorator-after-init pattern. Could be done via a new optional capability_filter callable on the lowlevel Server, or via an override at the MCPServer layer.

B. Register list handlers lazily.
Pass on_list_tools=None etc. from MCPServer.__init__; register via the existing Server._add_request_handler on the first add_tool() / add_resource() / add_prompt(). Matches the documented "offers any X" schema semantics directly, no post-processing needed. This shifts one usage contract: primitives added via add_*() after run() would not appear in capabilities. In practice primitives are registered before run(), and post-handshake capability changes aren't supported by the spec regardless, so the regression surface is narrow — but it's worth calling out.

I'd like to take this on if you're open to a fix — please assign to me after triage, and let me know if you'd prefer a direction other than A or B.

Python & MCP Python SDK

Python 3.13
Reproduced on: v1.27.0 (released), main @ 3d7b311 (2026-04-14)

Activity

  1. added a commit that references this issue on May 21, 2026
    e187a74
  2. added
    bugSomething isn't working
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Aug 14, 2026
  3. PRINCE2-AI commented on Oct 26, 2026

    @PRINCE2-AI

    Confirmed on main @ a525a3d (2026-10-24), Python 3.12, using the probe from the report unchanged:

    • MCPServer("empty") → {"prompts":{"listChanged":false},"resources":{"listChanged":false,"subscribe":false},"tools":{"listChanged":false}}
    • a server with one @mcp.tool() → exactly the same capabilities, so a client cannot distinguish the two.

    Still the unconditional on_list_tools / on_list_resources / on_list_prompts wiring in MCPServer.__init__: all three handler keys exist, so get_capabilities() always emits all three entries.

    Since tools/prompts/resources can be registered after construction and before run(), computing the flags at __init__ would just move the bug. Deriving them at handshake time from the registries, or registering the list handlers lazily on first add_*, seem like the two options.

    1. Is the intended direction the narrow gate on the list handlers, or the broader "derive capabilities from server state" refactor in the lowlevel/server.py TODO?
    2. Should tools also be omitted for an empty server, or is an always-present empty tools capability considered acceptable?

    Happy to test a patch against empty / tools-only / resources-only / prompts-only, plus a resource added after construction, if that direction works for you.

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 working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions