modelcontextprotocol / modelcontextprotocol/python-sdk
Duplicate `initialize` with changed parameters can overwrite `ServerSession.client_params`
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
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
After a normal MCP initialization flow, a Python SDK server accepts another initialize request on the same live connection/session and returns a normal initialize result.
I agree that the spec does not explicitly say every repeated initialize must be rejected. My concern is narrower: after the session has already moved into normal operation, a later same-session initialize with different parameters can update the server's stored client initialization parameters.
The MCP lifecycle documentation describes initialization as the first client/server interaction and says the operation phase uses the negotiated protocol version and capabilities. In that model, silently changing the stored client identity/capabilities/protocol version after operation has begun seems surprising unless the SDK intentionally supports same-session renegotiation.
src/mcp/server/session.pystores the client initialization request inServerSession._client_params.ServerSession.client_paramsexposes that value.- In both the stable release and the
mainsnapshot I tested, theInitializeRequesthandler assignsself._client_params = params. ServerSession.check_client_capability()later readsself._client_params.capabilities, so a duplicateinitializewith differentcapabilitiescan affect capability-gated server logic.
What I observed
Reproduced on stable v1.27.1 (77431ebe7dda9ed0c61451b22d3e7f8d981bc092):
stdio: a secondinitializewas accepted afternotifications/initialized; a laterpingstill succeeded.- stateful Streamable HTTP: a second
initializewas accepted afternotifications/initialized; a laterpingstill succeeded. - stateless Streamable HTTP: a second
initializewas accepted afternotifications/initialized; a laterpingstill succeeded.
For stateful Streamable HTTP on v1.27.1, I added a local diagnostic tool that reads ctx.session.client_params before and after the duplicate initialize on the same session:
Duplicate initialize parameters |
Observed result |
|---|---|
| Same parameters | No state change |
Changed clientInfo and capabilities |
client_params.clientInfo and client_params.capabilities changed |
Older protocolVersion (2024-11-05) |
client_params.protocolVersion and client_params.clientInfo changed |
On the frozen main snapshot (161834d4aee2633c42d3976c8f8751b6c4d947d5), the same duplicate-initialize acceptance still reproduced. My diagnostic snapshot directly observed client_params.capabilities changing after a duplicate initialize with changed capabilities on both stdio and stateful Streamable HTTP.
Expected behavior
If same-session duplicate initialization is not intended, I would expect the server to reject the later initialize after normal operation has begun, for example with a JSON-RPC -32600 Invalid Request error.
If this tolerant behavior is intentional, I think it should be either:
- a no-op once the session has already initialized, especially for changed parameters, or
- documented as same-session renegotiation, including whether
ServerSession.client_params, client capabilities, client identity, and protocol version are allowed to change.
Example Code
import json
try:
# Current main branch naming.
from mcp.server.mcpserver import Context, MCPServer
except ImportError:
# Latest stable release naming.
from mcp.server.fastmcp import Context, FastMCP as MCPServer
mcp = MCPServer("dup-init-repro")
def field(obj, *names):
if obj is None:
return None
for name in names:
if hasattr(obj, name):
return getattr(obj, name)
return None
def dump(value):
if value is None:
return None
if hasattr(value, "model_dump"):
return value.model_dump(mode="json", by_alias=True)
return value
@mcp.tool()
async def session_snapshot(ctx: Context) -> str:
params = ctx.session.client_params
return json.dumps(
{
"protocolVersion": field(params, "protocolVersion", "protocol_version"),
"clientInfo": dump(field(params, "clientInfo", "client_info")),
"clientCapabilities": dump(field(params, "capabilities")),
},
separators=(",", ":"),
default=str,
)
if __name__ == "__main__":
mcp.run(transport="streamable-http")
'''
Request sequence:
1. Start the server.
2. Send a normal `initialize`.
3. Send `notifications/initialized`.
4. Call the `session_snapshot` tool and record the returned JSON.
5. Send another `initialize` on the same live session, but change `clientInfo` and/or `capabilities`.
6. Call `session_snapshot` again on the same session.
On `v1.27.1`, the second snapshot reflects the changed `clientInfo` / `capabilities`. On the `main` snapshot I tested, the second snapshot still reflects changed `capabilities`.
'''
Python & MCP Python SDK
- Python: `3.12.3`
- MCP Python SDK: `mcp==1.27.1` (`v1.27.1`, `77431ebe7dda9ed0c61451b22d3e7f8d981bc092`)
- Also reproduced on `main` snapshot `161834d4aee2633c42d3976c8f8751b6c4d947d5` (`v1.25.0-159-g161834d4`, pinned 2026-05-11)
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/session.py, en particulier le gestionnaire de InitializeRequest et ServerSession.client_params. Reproduisez la séquence de double initialisation décrite dans l’issue avec stdio ainsi qu’avec Streamable HTTP stateful ou stateless, puis examinez comment les paramètres du client sont lus ensuite. Le travail est terminé lorsque le comportement choisi pour la double initialisation est cohérent et qu’aucun paramètre du client n’est écrasé involontairement.
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é
- Active
- Clarté
- Plutôt claire
- Accessibilité débutants
- 48/100