Skip to content

Commit 20b21d5

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 be7482b commit 20b21d5

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
@@ -303,7 +303,7 @@ async def test_a_non_loopback_http_redirect_uri_is_accepted_at_registration(
303303

304304
@requirement("hosting:auth:as:register-echo-application-type")
305305
async def test_register_echoes_native_for_a_client_that_registered_application_type_web(
306-
as_app: tuple[httpx.AsyncClient, InMemoryAuthorizationServerProvider],
306+
as_app: tuple[httpx2.AsyncClient, InMemoryAuthorizationServerProvider],
307307
) -> None:
308308
"""A client registering `application_type: "web"` is told `"native"` in the registration echo.
309309

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,
@@ -684,9 +683,9 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
684683

685684
server = Server("sentinel-name", on_list_tools=list_tools, on_call_tool=call_tool)
686685

687-
requests: list[httpx.Request] = []
686+
requests: list[httpx2.Request] = []
688687

689-
async def on_request(request: httpx.Request) -> None:
688+
async def on_request(request: httpx2.Request) -> None:
690689
requests.append(request)
691690

692691
discover = DiscoverResult(
@@ -717,9 +716,9 @@ async def test_sentinel_lookalike_argument_value_is_base64_wrapped_in_its_param_
717716
718717
Spec-mandated by the sentinel-collision rule, the only encoding trigger: the value is otherwise header-safe ASCII.
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(
@@ -757,9 +756,9 @@ async def test_null_and_absent_annotated_arguments_emit_no_param_headers_and_the
757756
argument against its `Mcp-Param-*` header and would reject an orphan header for the null or
758757
absent argument (a header matching no annotation is ignored).
759758
"""
760-
requests: list[httpx.Request] = []
759+
requests: list[httpx2.Request] = []
761760

762-
async def on_request(request: httpx.Request) -> None:
761+
async def on_request(request: httpx2.Request) -> None:
763762
requests.append(request)
764763

765764
discover = DiscoverResult(
@@ -971,11 +970,11 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
971970
with anyio.fail_after(5):
972971
async with (
973972
mounted_app(Server("modern", on_call_tool=call_tool)) as (http, _),
974-
aconnect_sse(
975-
http, "POST", "/mcp", json=body, headers=_modern_headers(method="tools/call", name="noisy")
973+
http.sse(
974+
"/mcp", method="POST", json=body, headers=_modern_headers(method="tools/call", name="noisy")
976975
) as source,
977976
):
978-
events = [event async for event in source.aiter_sse()]
977+
events = [event async for event in source]
979978

980979
assert source.response.status_code == 200
981980
assert source.response.headers["content-type"].split(";", 1)[0] == "text/event-stream"
@@ -1023,25 +1022,25 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
10231022

10241023
server = Server("scoped", on_call_tool=call_tool)
10251024

1026-
emit_responses: list[httpx.Response] = []
1025+
emit_responses: list[httpx2.Response] = []
10271026
emit_frames: list[JSONRPCMessage] = []
1028-
quiet_responses: list[httpx.Response] = []
1027+
quiet_responses: list[httpx2.Response] = []
10291028

1030-
async def post_emit(http: httpx.AsyncClient) -> None:
1029+
async def post_emit(http: httpx2.AsyncClient) -> None:
10311030
body = {
10321031
"jsonrpc": "2.0",
10331032
"id": 1,
10341033
"method": "tools/call",
10351034
"params": {"name": "emit", "arguments": {}, "_meta": _meta_envelope()},
10361035
}
1037-
async with aconnect_sse(
1038-
http, "POST", "/mcp", json=body, headers=_modern_headers(method="tools/call", name="emit")
1036+
async with http.sse(
1037+
"/mcp", method="POST", json=body, headers=_modern_headers(method="tools/call", name="emit")
10391038
) as source:
1040-
events = [event async for event in source.aiter_sse()]
1039+
events = [event async for event in source]
10411040
emit_responses.append(source.response)
10421041
emit_frames.extend(parse_sse_messages(events))
10431042

1044-
async def post_quiet(http: httpx.AsyncClient) -> None:
1043+
async def post_quiet(http: httpx2.AsyncClient) -> None:
10451044
body = {
10461045
"jsonrpc": "2.0",
10471046
"id": 2,
@@ -1111,12 +1110,12 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
11111110
with anyio.fail_after(5):
11121111
async with (
11131112
mounted_app(Server("modern", on_call_tool=call_tool)) as (http, _),
1114-
aconnect_sse(
1115-
http, "POST", "/mcp", json=body, headers=_modern_headers(method="tools/call", name="noisy")
1113+
http.sse(
1114+
"/mcp", method="POST", json=body, headers=_modern_headers(method="tools/call", name="noisy")
11161115
) as source,
11171116
):
11181117
# Drained only so teardown is clean.
1119-
async for _ in source.aiter_sse():
1118+
async for _ in source:
11201119
pass
11211120

11221121
assert source.response.headers["x-accel-buffering"] == "no"
@@ -1227,7 +1226,7 @@ async def test_modern_encoded_mcp_name_matching_the_body_after_decode_is_served(
12271226
"""A sentinel-encoded ``Mcp-Name`` whose decoded value matches the body is served, not rejected.
12281227
12291228
Spec-mandated: the server decodes the header before validation -- a plain string comparison
1230-
would have answered 400 HeaderMismatch. The typed client sends ASCII bare, hence raw httpx.
1229+
would have answered 400 HeaderMismatch. The typed client sends ASCII bare, hence raw httpx2.
12311230
"""
12321231
body = {
12331232
"jsonrpc": "2.0",
@@ -1262,9 +1261,9 @@ async def get_prompt(ctx: ServerRequestContext, params: GetPromptRequestParams)
12621261

12631262
server = Server("sentinel-prompt", on_get_prompt=get_prompt)
12641263

1265-
requests: list[httpx.Request] = []
1264+
requests: list[httpx2.Request] = []
12661265

1267-
async def on_request(request: httpx.Request) -> None:
1266+
async def on_request(request: httpx2.Request) -> None:
12681267
requests.append(request)
12691268

12701269
discover = DiscoverResult(
@@ -1324,11 +1323,11 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
13241323
}
13251324
with anyio.fail_after(5):
13261325
async with mounted_app(Server("modern", on_call_tool=call_tool)) as (http, _):
1327-
async with aconnect_sse(
1328-
http, "POST", "/mcp", json=body, headers=_modern_headers(method="tools/call", name="park")
1326+
async with http.sse(
1327+
"/mcp", method="POST", json=body, headers=_modern_headers(method="tools/call", name="park")
13291328
) as source:
13301329
# Advanced once only: a full `async for` would wait for the close that is ours to perform.
1331-
events = source.aiter_sse()
1330+
events = aiter(source)
13321331
first = await anext(events)
13331332
# Awaited while the app is still mounted: after mounted_app exits, the bridge's
13341333
# teardown cancellation would make this pass vacuously.
@@ -1458,7 +1457,7 @@ async def test_modern_mcp_param_header_disagreeing_with_body_argument_is_rejecte
14581457
14591458
Spec-mandated: the server resolves the ``x-mcp-header`` annotation from the tool's advertised
14601459
``inputSchema`` via its own tools/list handler and rejects the decoded-header/body disagreement
1461-
before dispatch. Raw httpx because the HTTP status is a wire-only observable and the typed
1460+
before dispatch. Raw httpx2 because the HTTP status is a wire-only observable and the typed
14621461
client cannot emit a mismatching header by construction.
14631462
"""
14641463

0 commit comments

Comments
 (0)