modelcontextprotocol / modelcontextprotocol/python-sdk
MCPServer unconditionally declares `prompts`, `resources`, `tools` capabilities on `initialize`
Personne n'a encore pris cette issue.
- Langage dominant
- Python
- Étoiles
- 24.3k
- Forks
- 4k
- Merge moyen
- 1 j 1 h
- PR mergées (30 j)
- 31
Description
Pre-flight
- Using the latest MCP Python SDK (
v1.27.0) and also verified againstmain @ 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):
src/mcp/server/mcpserver/server.py#L178-L184—MCPServer.__init__unconditionally wireson_list_tools=self._handle_list_tools,on_list_resources=...,on_list_prompts=...into the lowlevelServer(...).src/mcp/server/lowlevel/server.py#L205-L224—Server.__init__unconditionally populatesself._request_handlerswith"tools/list","resources/list","prompts/list"entries.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→ noprompts,resources, ortoolsentries.one_tool_server.py→toolspresent,promptsandresourcesabsent.
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)
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Piste de recherche
Commencez par src/mcp/server/mcpserver/server.py et src/mcp/server/lowlevel/server.py, en particulier l’initialisation de MCPServer, l’enregistrement des gestionnaires de requêtes et get_capabilities(). Reproduisez la réponse initialize avec les exemples empty_server.py et one_tool_server.py, puis examinez le TODO des capabilities de bas niveau. La tâche est terminée lorsque les capabilities reflètent les primitives effectivement enregistrées et que les exemples produisent les réponses attendues.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- python
- Domaine
- api, backend
- Type d'issue
- Bug
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- Calme
- Clarté
- Clairement spécifiée
- Accessibilité débutants
- 45/100