@@ -171,30 +171,35 @@ async def test_handle_modern_request_rejects_a_notification_post_at_an_unserved_
171171
172172
173173@pytest .mark .parametrize (
174- "body" ,
174+ ( "body" , "expected_id" ) ,
175175 [
176- pytest .param ({"jsonrpc" : "2.0" , "id" : 1 , "result" : {}}, id = "posted-response" ),
177- pytest .param ({"jsonrpc" : "2.0" , "id" : 1 , "error" : {"code" : - 1 , "message" : "x" }}, id = "posted-error" ),
178- pytest .param ([{"jsonrpc" : "2.0" , "method" : "notifications/cancelled" , "params" : {"requestId" : 1 }}], id = "batch" ),
179- pytest .param ({"jsonrpc" : "2.0" , "id" : None , "method" : "tools/list" }, id = "null-id-request" ),
180- pytest .param ({"jsonrpc" : "2.0" , "id" : [1 ], "method" : "tools/list" }, id = "non-scalar-id-request" ),
181- pytest .param ({"jsonrpc" : "2.0" , "method" : 7 }, id = "non-string-method-notification" ),
182- pytest .param ({"jsonrpc" : "1.0" , "method" : "notifications/cancelled" }, id = "wrong-jsonrpc-version" ),
183- pytest .param ("just a string" , id = "scalar" ),
176+ pytest .param ({"jsonrpc" : "2.0" , "id" : 1 , "result" : {}}, 1 , id = "posted-response" ),
177+ pytest .param ({"jsonrpc" : "2.0" , "id" : 1 , "error" : {"code" : - 1 , "message" : "x" }}, 1 , id = "posted-error" ),
178+ pytest .param (
179+ [{"jsonrpc" : "2.0" , "method" : "notifications/cancelled" , "params" : {"requestId" : 1 }}], None , id = "batch"
180+ ),
181+ pytest .param ({"jsonrpc" : "2.0" , "id" : None , "method" : "tools/list" }, None , id = "null-id-request" ),
182+ pytest .param ({"jsonrpc" : "2.0" , "id" : [1 ], "method" : "tools/list" }, None , id = "non-scalar-id-request" ),
183+ pytest .param ({"jsonrpc" : "2.0" , "method" : 7 }, None , id = "non-string-method-notification" ),
184+ pytest .param ({"jsonrpc" : "1.0" , "method" : "notifications/cancelled" }, None , id = "wrong-jsonrpc-version" ),
185+ pytest .param ("just a string" , None , id = "scalar" ),
184186 ],
185187)
186- async def test_handle_modern_request_rejects_a_body_that_is_neither_request_nor_notification (body : Any ) -> None :
188+ async def test_handle_modern_request_rejects_a_body_that_is_neither_request_nor_notification (
189+ body : Any , expected_id : int | None
190+ ) -> None :
187191 """Spec-mandated (streamable-http §Sending Messages item 4): the body MUST be a single request
188192 or notification and clients MUST NOT post responses. SDK-defined: anything else -- a posted
189193 response, a batch, a request whose `id` is malformed, a scalar -- is `INVALID_REQUEST` at
190- HTTP 400 with `id: null`, distinct from `PARSE_ERROR` (malformed JSON). A malformed-`id`
191- request in particular must not be mistaken for a notification and silently 202'd."""
194+ HTTP 400 with the original id echoed when one is recoverable (else `id: null`), distinct
195+ from `PARSE_ERROR` (malformed JSON). A malformed-`id` request in particular must not be
196+ mistaken for a notification and silently 202'd."""
192197 async with _asgi_client (Server ("test" )) as http :
193198 response = await http .post ("/mcp" , json = body )
194199 assert response .status_code == 400
195200 assert response .json () == {
196201 "jsonrpc" : "2.0" ,
197- "id" : None ,
202+ "id" : expected_id ,
198203 "error" : {"code" : INVALID_REQUEST , "message" : "Body must be a single JSON-RPC request or notification object" },
199204 }
200205
@@ -216,6 +221,27 @@ async def test_handle_modern_request_rejects_malformed_body_with_parse_error() -
216221 }
217222
218223
224+ @pytest .mark .parametrize (
225+ ("body" , "expected_id" ),
226+ [
227+ pytest .param ({"jsonrpc" : "1.0" , "id" : 3 , "method" : "ping" , "params" : {}}, 3 , id = "wrong-jsonrpc-version" ),
228+ pytest .param ({"id" : 4 , "method" : "ping" , "params" : {}}, 4 , id = "missing-jsonrpc-field" ),
229+ pytest .param ({"jsonrpc" : "2.0" , "id" : 8 , "method" : 12345 , "params" : {}}, 8 , id = "non-string-method" ),
230+ ],
231+ )
232+ async def test_handle_modern_request_envelope_invalid_request_echoes_the_original_id (
233+ body : dict [str , Any ], expected_id : int
234+ ) -> None :
235+ """An envelope-invalid but id-bearing request is answered -32600 with the
236+ original request id, so the client can correlate the failure."""
237+ async with _asgi_client (Server ("test" )) as http :
238+ response = await http .post ("/mcp" , json = body )
239+ assert response .status_code == 400
240+ error = response .json ()
241+ assert error ["id" ] == expected_id
242+ assert error ["error" ]["code" ] == INVALID_REQUEST
243+
244+
219245async def test_handle_modern_request_returns_transport_security_error_response () -> None :
220246 """The transport-security middleware's error response is sent verbatim and short-circuits."""
221247 settings = TransportSecuritySettings (enable_dns_rebinding_protection = True , allowed_hosts = ["good.example" ])
0 commit comments