Skip to content

Commit 394ed98

Browse files
committed
Reject changed duplicate initialize requests
Keep duplicate initialize requests idempotent when the client sends the same parameters, but return INVALID_REQUEST if a stateful server is reinitialized with different parameters after negotiation.
1 parent ba33472 commit 394ed98

2 files changed

Lines changed: 129 additions & 17 deletions

File tree

src/mcp/server/session.py

Lines changed: 32 additions & 17 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
)
@@ -172,30 +173,44 @@ async def _receive_loop(self) -> None:
172173
async with self._incoming_message_stream_writer:
173174
await super()._receive_loop()
174175

176+
def _initialize_result(self, requested_version: str | int) -> types.InitializeResult:
177+
return types.InitializeResult(
178+
protocolVersion=requested_version
179+
if requested_version in SUPPORTED_PROTOCOL_VERSIONS
180+
else types.LATEST_PROTOCOL_VERSION,
181+
capabilities=self._init_options.capabilities,
182+
serverInfo=types.Implementation(
183+
name=self._init_options.server_name,
184+
version=self._init_options.server_version,
185+
websiteUrl=self._init_options.website_url,
186+
icons=self._init_options.icons,
187+
),
188+
instructions=self._init_options.instructions,
189+
)
190+
175191
async def _received_request(self, responder: RequestResponder[types.ClientRequest, types.ServerResult]):
176192
match responder.request.root:
177193
case types.InitializeRequest(params=params):
194+
if not self._stateless and self._initialization_state == InitializationState.Initialized:
195+
if params == self._client_params:
196+
with responder:
197+
await responder.respond(types.ServerResult(self._initialize_result(params.protocolVersion)))
198+
return
199+
200+
with responder:
201+
await responder.respond(
202+
types.ErrorData(
203+
code=types.INVALID_REQUEST,
204+
message="Server is already initialized",
205+
)
206+
)
207+
return
208+
178209
requested_version = params.protocolVersion
179210
self._initialization_state = InitializationState.Initializing
180211
self._client_params = params
181212
with responder:
182-
await responder.respond(
183-
types.ServerResult(
184-
types.InitializeResult(
185-
protocolVersion=requested_version
186-
if requested_version in SUPPORTED_PROTOCOL_VERSIONS
187-
else types.LATEST_PROTOCOL_VERSION,
188-
capabilities=self._init_options.capabilities,
189-
serverInfo=types.Implementation(
190-
name=self._init_options.server_name,
191-
version=self._init_options.server_version,
192-
websiteUrl=self._init_options.website_url,
193-
icons=self._init_options.icons,
194-
),
195-
instructions=self._init_options.instructions,
196-
)
197-
)
198-
)
213+
await responder.respond(types.ServerResult(self._initialize_result(requested_version)))
199214
self._initialization_state = InitializationState.Initialized
200215
case types.PingRequest():
201216
# Ping requests are allowed at any time

tests/server/test_session.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,103 @@ 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_preserves_negotiated_params():
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+
with anyio.fail_after(5):
274+
first_response = await server_to_client_receive.receive()
275+
assert isinstance(first_response.message.root, types.JSONRPCResponse)
276+
277+
await client_to_server_send.send(
278+
SessionMessage(
279+
types.JSONRPCMessage(
280+
types.JSONRPCNotification(
281+
jsonrpc="2.0",
282+
method="notifications/initialized",
283+
)
284+
)
285+
)
286+
)
287+
288+
await client_to_server_send.send(initialize_message(2, "client-a", first_capabilities))
289+
290+
with anyio.fail_after(5):
291+
second_response = await server_to_client_receive.receive()
292+
assert isinstance(second_response.message.root, types.JSONRPCResponse)
293+
294+
await client_to_server_send.send(
295+
initialize_message(3, "client-b", types.ClientCapabilities(), protocol_version="2024-11-05")
296+
)
297+
298+
with anyio.fail_after(5):
299+
third_response = await server_to_client_receive.receive()
300+
assert isinstance(third_response.message.root, types.JSONRPCError)
301+
assert third_response.message.root.error.code == types.INVALID_REQUEST
302+
303+
await client_to_server_send.aclose()
304+
305+
async with (
306+
client_to_server_send,
307+
client_to_server_receive,
308+
server_to_client_send,
309+
server_to_client_receive,
310+
anyio.create_task_group() as tg,
311+
):
312+
tg.start_soon(run_server)
313+
tg.start_soon(mock_client)
314+
315+
assert final_client_params is not None
316+
assert final_client_params.clientInfo.name == "client-a"
317+
assert final_client_params.protocolVersion == types.LATEST_PROTOCOL_VERSION
318+
319+
223320
@pytest.mark.anyio
224321
async def test_ping_request_before_initialization():
225322
"""Test that ping requests are allowed before initialization is complete."""

0 commit comments

Comments
 (0)