modelcontextprotocol / modelcontextprotocol/python-sdk

MCPServer unconditionally declares `prompts`, `resources`, `tools` capabilities on `initialize`

Open
#2,473 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug P2
Dominant language
Python
Stars
24.3k
Forks
4k
Avg merge
1d 1h
Merged PRs (30d)
31

Description

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-L184MCPServer.__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-L224Server.__init__ unconditionally populates self._request_handlers with "tools/list", "resources/list", "prompts/list" entries.
  3. src/mcp/server/lowlevel/server.py#L283-L328get_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.pytools 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)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/mcp/server/mcpserver/server.py and src/mcp/server/lowlevel/server.py, especially MCPServer initialization, request-handler registration, and get_capabilities(). Reproduce the initialize response with the empty_server.py and one_tool_server.py examples, then review the low-level capabilities TODO. Done means capabilities reflect the primitives actually registered and the examples produce the expected responses.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.