Skip to content

Duplicate initialize with changed parameters can overwrite ServerSession.client_params #2605

Description

@cclabadmin

Initial Checks

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.py stores the client initialization request in ServerSession._client_params.
  • ServerSession.client_params exposes that value.
  • In both the stable release and the main snapshot I tested, the InitializeRequest handler assigns self._client_params = params.
  • ServerSession.check_client_capability() later reads self._client_params.capabilities, so a duplicate initialize with different capabilities can affect capability-gated server logic.

What I observed

Reproduced on stable v1.27.1 (77431ebe7dda9ed0c61451b22d3e7f8d981bc092):

  • stdio: a second initialize was accepted after notifications/initialized; a later ping still succeeded.
  • stateful Streamable HTTP: a second initialize was accepted after notifications/initialized; a later ping still succeeded.
  • stateless Streamable HTTP: a second initialize was accepted after notifications/initialized; a later ping still 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)

Activity

  1. jshaofa-ui commented on May 15, 2026

    @jshaofa-ui
  2. added
    triageQueued for automated analysis — bot will process and remove this label
    on May 31, 2026
  3. ghost added
    bugSomething isn't working
    on Jun 1, 2026
  4. ghost added
    ready for workEnough information for someone to start working on
    on Jun 1, 2026
  5. ghost added
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Jun 1, 2026
  6. ghost added
    fix proposedBot has a verified fix diff in the comment
    on Jun 1, 2026
  7. ghost removed
    triageQueued for automated analysis — bot will process and remove this label
    on Jun 1, 2026
  8. added 3 commits that reference this issue on Jun 26, 2026
    cf7b17f
    c8375b5
    1e2bd9b
  9. ychampion commented on Jul 8, 2026

    @ychampion

    I’m going to work on a focused fix for this: reject a duplicate initialize after the session is initialized, and add a regression that the original client_params stay intact.

  10. jadhavgaurav commented on Aug 25, 2026

    @jadhavgaurav

    Confirmed against current main: _on_request in src/mcp/server/runner.py commits self.connection.client_params, self.connection.protocol_version = self._negotiate_initialize(params) on every successful initialize call, with no check for whether the connection was already initialized. _handle_initialize doesn't guard either, so a second initialize with different clientInfo/capabilities silently overwrites the negotiated params for the life of the connection.

    On the two open PRs:

    • fix(server): reject changed duplicate initialize #3078 modifies ServerSession._received_request in src/mcp/server/session.py (_client_params, _initialization_state). That internal state machine isn't where initialize is handled anymore — session.py's client_params is now a thin read-only proxy onto Connection.client_params (src/mcp/server/connection.py), and the actual handling lives in runner.py. The PR predates that move, so it doesn't apply to current main.
    • fix: reject changed duplicate initialize #2606 patches the right spot (_handle_initialize in runner.py), guarding on self.connection.client_params is not None and init != self.connection.client_params. That's correct for the common case and its test (test_runner_rejects_changed_duplicate_initialize) matches the architecture. One thing worth checking before merge: the guard runs inside _handle_initialize, which executes before the state commit at the end of _on_request (line ~239), so a rejected duplicate correctly leaves connection.client_params untouched — but it's worth an explicit test asserting the unchanged value post-rejection (matching the issue title), not just that an error is raised.

    Proposed approach if this needs a fresh PR: same guard as #2606 (compare negotiated InitializeRequestParams against self.connection.client_params when already set, reject with INVALID_REQUEST on mismatch, no-op-accept on an identical repeat), plus a regression test in tests/server/test_runner.py that explicitly asserts connection.client_params is unchanged after the rejected duplicate.

    AI disclosure: this comment and the accompanying analysis were produced with AI assistance (Claude Code), reviewed by me before posting.

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