Skip to content

Commit fd8d08d

Browse files
committed
Reject duplicate initialize after negotiation
Duplicate initialize requests could rewrite ServerSession.client_params after the session entered normal operation. The handler now returns a JSON-RPC invalid-request error for stateful sessions that are already initialized, while preserving stateless sessions that intentionally start initialized. Constraint: v1.x bug fix for issue #2605; stateless streamable HTTP sessions must keep their existing initialization behavior. Rejected: Reject every initialize request while the session state is Initialized | stateless sessions start in Initialized and still need to handle per-request initialize messages. Confidence: high Scope-risk: narrow Directive: Do not allow stateful sessions to renegotiate client parameters after initialization has completed. Tested: uv run pytest -n0 tests/server/test_session.py::test_duplicate_initialize_after_initialized_is_rejected -q Tested: uv run pytest -n0 tests/server/test_session.py -q Tested: uv run pytest -n0 tests/server/test_session_race_condition.py tests/server/test_streamable_http_manager.py::test_stateless_requests_memory_cleanup -q Tested: uv run ruff format --check src/mcp/server/session.py tests/server/test_session.py Tested: uv run ruff check src/mcp/server/session.py tests/server/test_session.py Tested: uv run pyright src/mcp/server/session.py tests/server/test_session.py Not-tested: full repository test suite
1 parent ba33472 commit fd8d08d

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

src/mcp/server/session.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def __init__(
9494
stateless: bool = False,
9595
) -> None:
9696
super().__init__(read_stream, write_stream, types.ClientRequest, types.ClientNotification)
97+
self._stateless = stateless
9798
self._initialization_state = (
9899
InitializationState.Initialized if stateless else InitializationState.NotInitialized
99100
)
@@ -175,6 +176,16 @@ async def _receive_loop(self) -> None:
175176
async def _received_request(self, responder: RequestResponder[types.ClientRequest, types.ServerResult]):
176177
match responder.request.root:
177178
case types.InitializeRequest(params=params):
179+
if not self._stateless and self._initialization_state == InitializationState.Initialized:
180+
with responder:
181+
await responder.respond(
182+
types.ErrorData(
183+
code=types.INVALID_REQUEST,
184+
message="Server is already initialized",
185+
)
186+
)
187+
return
188+
178189
requested_version = params.protocolVersion
179190
self._initialization_state = InitializationState.Initializing
180191
self._client_params = params

tests/server/test_session.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,95 @@ async def mock_client():
220220
assert received_protocol_version == "2024-11-05"
221221

222222

223+
@pytest.mark.anyio
224+
async def test_duplicate_initialize_after_initialized_is_rejected():
225+
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)
226+
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](10)
227+
first_capabilities = types.ClientCapabilities(sampling=types.SamplingCapability())
228+
final_client_params: types.InitializeRequestParams | None = None
229+
230+
def initialize_message(
231+
request_id: int,
232+
client_name: str,
233+
capabilities: types.ClientCapabilities,
234+
protocol_version: str = types.LATEST_PROTOCOL_VERSION,
235+
) -> SessionMessage:
236+
return SessionMessage(
237+
types.JSONRPCMessage(
238+
types.JSONRPCRequest(
239+
jsonrpc="2.0",
240+
id=request_id,
241+
method="initialize",
242+
params=types.InitializeRequestParams(
243+
protocolVersion=protocol_version,
244+
capabilities=capabilities,
245+
clientInfo=types.Implementation(name=client_name, version="1.0.0"),
246+
).model_dump(by_alias=True, mode="json", exclude_none=True),
247+
)
248+
)
249+
)
250+
251+
async def run_server():
252+
nonlocal final_client_params
253+
254+
async with ServerSession(
255+
client_to_server_receive,
256+
server_to_client_send,
257+
InitializationOptions(
258+
server_name="mcp",
259+
server_version="0.1.0",
260+
capabilities=ServerCapabilities(),
261+
),
262+
) as server_session:
263+
async for message in server_session.incoming_messages:
264+
if isinstance(message, Exception): # pragma: no cover
265+
raise message
266+
267+
final_client_params = server_session.client_params
268+
assert server_session.check_client_capability(first_capabilities)
269+
270+
async def mock_client():
271+
await client_to_server_send.send(initialize_message(1, "client-a", first_capabilities))
272+
273+
first_response = await server_to_client_receive.receive()
274+
assert isinstance(first_response.message.root, types.JSONRPCResponse)
275+
276+
await client_to_server_send.send(
277+
SessionMessage(
278+
types.JSONRPCMessage(
279+
types.JSONRPCNotification(
280+
jsonrpc="2.0",
281+
method="notifications/initialized",
282+
)
283+
)
284+
)
285+
)
286+
287+
await client_to_server_send.send(
288+
initialize_message(2, "client-b", types.ClientCapabilities(), protocol_version="2024-11-05")
289+
)
290+
291+
second_response = await server_to_client_receive.receive()
292+
assert isinstance(second_response.message.root, types.JSONRPCError)
293+
assert second_response.message.root.error.code == types.INVALID_REQUEST
294+
295+
await client_to_server_send.aclose()
296+
297+
async with (
298+
client_to_server_send,
299+
client_to_server_receive,
300+
server_to_client_send,
301+
server_to_client_receive,
302+
anyio.create_task_group() as tg,
303+
):
304+
tg.start_soon(run_server)
305+
tg.start_soon(mock_client)
306+
307+
assert final_client_params is not None
308+
assert final_client_params.clientInfo.name == "client-a"
309+
assert final_client_params.protocolVersion == types.LATEST_PROTOCOL_VERSION
310+
311+
223312
@pytest.mark.anyio
224313
async def test_ping_request_before_initialization():
225314
"""Test that ping requests are allowed before initialization is complete."""

0 commit comments

Comments
 (0)