Skip to content

Commit 0f81299

Browse files
committed
Reconcile the interaction suite with main's httpx2 rename and re-drive the era-gate test through the public request seam
Port the branch-authored httpx/httpx_sse lines main's rename never saw to httpx2, including the four aconnect_sse readers in the modern hosting tests, and delete the dead httpx_sse import. Drive the 2026 resources/subscribe era-gate test through ClientSession.send_request instead of the deprecated typed subscribe wrapper.
1 parent 074c99e commit 0f81299

5 files changed

Lines changed: 35 additions & 33 deletions

File tree

‎tests/interaction/auth/test_as_handlers.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ async def test_a_non_loopback_http_redirect_uri_is_accepted_at_registration(
335335

336336
@requirement("hosting:auth:as:register-echo-application-type")
337337
async def test_register_echoes_native_for_a_client_that_registered_application_type_web(
338-
as_app: tuple[httpx.AsyncClient, InMemoryAuthorizationServerProvider],
338+
as_app: tuple[httpx2.AsyncClient, InMemoryAuthorizationServerProvider],
339339
) -> None:
340340
"""A client registering `application_type: "web"` is told `"native"` in the registration echo.
341341

‎tests/interaction/auth/test_flow.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ async def test_dcr_defaults_grant_types_to_authorization_code_and_refresh_token_
221221
222222
The metadata is built directly rather than via `oauth_client_metadata()`, which sets `grant_types`.
223223
"""
224-
requests: list[httpx.Request] = []
224+
requests: list[httpx2.Request] = []
225225
provider = InMemoryAuthorizationServerProvider()
226226
server = Server("guarded", on_list_tools=list_tools)
227227
client_metadata = OAuthClientMetadata(client_name="interaction-suite", redirect_uris=[AnyUrl(REDIRECT_URI)])
@@ -253,7 +253,7 @@ async def test_dcr_sends_consumer_set_grant_types_verbatim() -> None:
253253
254254
The value deliberately differs from the default pair so pass-through is distinguishable from defaulting.
255255
"""
256-
requests: list[httpx.Request] = []
256+
requests: list[httpx2.Request] = []
257257
provider = InMemoryAuthorizationServerProvider()
258258
server = Server("guarded", on_list_tools=list_tools)
259259
client_metadata = OAuthClientMetadata(
@@ -281,7 +281,7 @@ async def test_dcr_sends_a_consumer_set_application_type_verbatim() -> None:
281281
`"web"` against a loopback redirect URI is deliberately not what redirect-URI derivation
282282
would produce, so pass-through stays distinguishable from any future derivation strategy.
283283
"""
284-
requests: list[httpx.Request] = []
284+
requests: list[httpx2.Request] = []
285285
provider = InMemoryAuthorizationServerProvider()
286286
server = Server("guarded", on_list_tools=list_tools)
287287
client_metadata = OAuthClientMetadata(

‎tests/interaction/lowlevel/test_mrtr.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,7 @@ def _meta_envelope() -> dict[str, object]:
921921
async def test_retry_with_malformed_input_responses_is_rejected_with_invalid_params() -> None:
922922
"""A retry whose inputResponses do not parse is rejected with invalid params before dispatch (spec SHOULD).
923923
924-
Raw httpx against the mounted modern entry: the typed API rejects garbage inputResponses at
924+
Raw httpx2 against the mounted modern entry: the typed API rejects garbage inputResponses at
925925
construction, so the violation is unproducible above this seam.
926926
"""
927927

‎tests/interaction/lowlevel/test_resources.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,17 +264,20 @@ async def test_resources_subscribe_on_a_2026_connection_is_method_not_found_desp
264264
"""On a 2026-07-28 connection, `resources/subscribe` is METHOD_NOT_FOUND even with a handler registered.
265265
266266
resources/subscribe is removed from the 2026-07-28 surface; the registry rejects it before handler lookup.
267+
The request is sent through the generic `send_request` seam because the typed subscribe method
268+
is retired along with the 2025-era capability it drives.
267269
"""
268270

269271
async def subscribe_resource(ctx: ServerRequestContext, params: types.SubscribeRequestParams) -> EmptyResult:
270272
"""Registered so the rejection provably comes from the era gate, not a missing handler."""
271273
raise NotImplementedError
272274

273275
server = Server("library", on_subscribe_resource=subscribe_resource)
276+
subscribe = types.SubscribeRequest(params=types.SubscribeRequestParams(uri="file:///watched.txt"))
274277

275278
async with connect(server) as client:
276279
with pytest.raises(MCPError) as exc_info:
277-
await client.subscribe_resource("file:///watched.txt")
280+
await client.session.send_request(subscribe, EmptyResult)
278281

279282
assert exc_info.value.error == snapshot(
280283
ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="resources/subscribe")

‎tests/interaction/transports/test_hosting_http_modern.py‎

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import anyio
1515
import httpx2
1616
import pytest
17-
from httpx_sse import aconnect_sse
1817
from inline_snapshot import snapshot
1918
from mcp_types import (
2019
CLIENT_CAPABILITIES_META_KEY,
@@ -717,9 +716,9 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
717716

718717
server = Server("sentinel-name", on_list_tools=list_tools, on_call_tool=call_tool)
719718

720-
requests: list[httpx.Request] = []
719+
requests: list[httpx2.Request] = []
721720

722-
async def on_request(request: httpx.Request) -> None:
721+
async def on_request(request: httpx2.Request) -> None:
723722
requests.append(request)
724723

725724
discover = DiscoverResult(
@@ -750,9 +749,9 @@ async def test_sentinel_lookalike_argument_value_is_base64_wrapped_in_its_param_
750749
751750
Spec-mandated by the sentinel-collision rule, the only encoding trigger: the value is otherwise header-safe ASCII.
752751
"""
753-
requests: list[httpx.Request] = []
752+
requests: list[httpx2.Request] = []
754753

755-
async def on_request(request: httpx.Request) -> None:
754+
async def on_request(request: httpx2.Request) -> None:
756755
requests.append(request)
757756

758757
discover = DiscoverResult(
@@ -790,9 +789,9 @@ async def test_null_and_absent_annotated_arguments_emit_no_param_headers_and_the
790789
argument against its `Mcp-Param-*` header and would reject an orphan header for the null or
791790
absent argument (a header matching no annotation is ignored).
792791
"""
793-
requests: list[httpx.Request] = []
792+
requests: list[httpx2.Request] = []
794793

795-
async def on_request(request: httpx.Request) -> None:
794+
async def on_request(request: httpx2.Request) -> None:
796795
requests.append(request)
797796

798797
discover = DiscoverResult(
@@ -1004,11 +1003,11 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
10041003
with anyio.fail_after(5):
10051004
async with (
10061005
mounted_app(Server("modern", on_call_tool=call_tool)) as (http, _),
1007-
aconnect_sse(
1008-
http, "POST", "/mcp", json=body, headers=_modern_headers(method="tools/call", name="noisy")
1006+
http.sse(
1007+
"/mcp", method="POST", json=body, headers=_modern_headers(method="tools/call", name="noisy")
10091008
) as source,
10101009
):
1011-
events = [event async for event in source.aiter_sse()]
1010+
events = [event async for event in source]
10121011

10131012
assert source.response.status_code == 200
10141013
assert source.response.headers["content-type"].split(";", 1)[0] == "text/event-stream"
@@ -1056,25 +1055,25 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
10561055

10571056
server = Server("scoped", on_call_tool=call_tool)
10581057

1059-
emit_responses: list[httpx.Response] = []
1058+
emit_responses: list[httpx2.Response] = []
10601059
emit_frames: list[JSONRPCMessage] = []
1061-
quiet_responses: list[httpx.Response] = []
1060+
quiet_responses: list[httpx2.Response] = []
10621061

1063-
async def post_emit(http: httpx.AsyncClient) -> None:
1062+
async def post_emit(http: httpx2.AsyncClient) -> None:
10641063
body = {
10651064
"jsonrpc": "2.0",
10661065
"id": 1,
10671066
"method": "tools/call",
10681067
"params": {"name": "emit", "arguments": {}, "_meta": _meta_envelope()},
10691068
}
1070-
async with aconnect_sse(
1071-
http, "POST", "/mcp", json=body, headers=_modern_headers(method="tools/call", name="emit")
1069+
async with http.sse(
1070+
"/mcp", method="POST", json=body, headers=_modern_headers(method="tools/call", name="emit")
10721071
) as source:
1073-
events = [event async for event in source.aiter_sse()]
1072+
events = [event async for event in source]
10741073
emit_responses.append(source.response)
10751074
emit_frames.extend(parse_sse_messages(events))
10761075

1077-
async def post_quiet(http: httpx.AsyncClient) -> None:
1076+
async def post_quiet(http: httpx2.AsyncClient) -> None:
10781077
body = {
10791078
"jsonrpc": "2.0",
10801079
"id": 2,
@@ -1144,12 +1143,12 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
11441143
with anyio.fail_after(5):
11451144
async with (
11461145
mounted_app(Server("modern", on_call_tool=call_tool)) as (http, _),
1147-
aconnect_sse(
1148-
http, "POST", "/mcp", json=body, headers=_modern_headers(method="tools/call", name="noisy")
1146+
http.sse(
1147+
"/mcp", method="POST", json=body, headers=_modern_headers(method="tools/call", name="noisy")
11491148
) as source,
11501149
):
11511150
# Drained only so teardown is clean.
1152-
async for _ in source.aiter_sse():
1151+
async for _ in source:
11531152
pass
11541153

11551154
assert source.response.headers["x-accel-buffering"] == "no"
@@ -1260,7 +1259,7 @@ async def test_modern_encoded_mcp_name_matching_the_body_after_decode_is_served(
12601259
"""A sentinel-encoded ``Mcp-Name`` whose decoded value matches the body is served, not rejected.
12611260
12621261
Spec-mandated: the server decodes the header before validation -- a plain string comparison
1263-
would have answered 400 HeaderMismatch. The typed client sends ASCII bare, hence raw httpx.
1262+
would have answered 400 HeaderMismatch. The typed client sends ASCII bare, hence raw httpx2.
12641263
"""
12651264
body = {
12661265
"jsonrpc": "2.0",
@@ -1295,9 +1294,9 @@ async def get_prompt(ctx: ServerRequestContext, params: GetPromptRequestParams)
12951294

12961295
server = Server("sentinel-prompt", on_get_prompt=get_prompt)
12971296

1298-
requests: list[httpx.Request] = []
1297+
requests: list[httpx2.Request] = []
12991298

1300-
async def on_request(request: httpx.Request) -> None:
1299+
async def on_request(request: httpx2.Request) -> None:
13011300
requests.append(request)
13021301

13031302
discover = DiscoverResult(
@@ -1357,11 +1356,11 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
13571356
}
13581357
with anyio.fail_after(5):
13591358
async with mounted_app(Server("modern", on_call_tool=call_tool)) as (http, _):
1360-
async with aconnect_sse(
1361-
http, "POST", "/mcp", json=body, headers=_modern_headers(method="tools/call", name="park")
1359+
async with http.sse(
1360+
"/mcp", method="POST", json=body, headers=_modern_headers(method="tools/call", name="park")
13621361
) as source:
13631362
# Advanced once only: a full `async for` would wait for the close that is ours to perform.
1364-
events = source.aiter_sse()
1363+
events = aiter(source)
13651364
first = await anext(events)
13661365
# Awaited while the app is still mounted: after mounted_app exits, the bridge's
13671366
# teardown cancellation would make this pass vacuously.
@@ -1491,7 +1490,7 @@ async def test_modern_mcp_param_header_disagreeing_with_body_argument_is_rejecte
14911490
14921491
Spec-mandated: the server resolves the ``x-mcp-header`` annotation from the tool's advertised
14931492
``inputSchema`` via its own tools/list handler and rejects the decoded-header/body disagreement
1494-
before dispatch. Raw httpx because the HTTP status is a wire-only observable and the typed
1493+
before dispatch. Raw httpx2 because the HTTP status is a wire-only observable and the typed
14951494
client cannot emit a mismatching header by construction.
14961495
"""
14971496

0 commit comments

Comments
 (0)