Skip to content

Commit 72edf90

Browse files
committed
Close the register-echo divergence and reconcile the cancellation suite with main
Reconcile the branch with main's landed behaviour on top of the rebase: - Adopt main's hosting:auth:as:register-echo entry over the branch's register-echo-application-type entry and its now-closed divergence, drop the stale pinning test that superseded it, and remove its unused imports. - Rewrite the no-further-notifications cancellation test onto the doomed-call pattern: a cancelled request is no longer answered, so the call is parked and abandoned instead of expecting an error, tolerating only the legacy streamable HTTP terminator. - Reword the no-further-notifications note to match the narrowed in-flight divergence. - Assert the persisted application_type echo in the DCR pass-through test now that the registration echo carries it. No-Verification-Needed: test-only diff, no runtime surface to drive
1 parent 77b6a97 commit 72edf90

4 files changed

Lines changed: 29 additions & 35 deletions

File tree

tests/interaction/_requirements.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -908,8 +908,9 @@ def __post_init__(self) -> None:
908908
"The 2026-07-28 stdio page states the receiver-side rule as 'MUST NOT send any "
909909
"further messages for it', strengthening the cancellation page's SHOULD-shaped "
910910
"receiver bullets (both revisions); the response half of 'any further messages' is "
911-
"the divergence recorded on protocol:cancel:in-flight (both seats answer a cancelled "
912-
"request with a code-0 error response). This entry pins the notifications half: the "
911+
"honoured too - a cancelled request goes unanswered - except the era-scoped legacy "
912+
"streamable HTTP terminator recorded as the Divergence on protocol:cancel:in-flight. "
913+
"This entry pins the notifications half: the "
913914
"cancellation stops the handler, so a send attempted during its unwind is itself "
914915
"cancelled before transmitting. Era-unbounded: the enforcement is the shared "
915916
"handler-scope cancellation, observable on the arms where notifications/cancelled "

tests/interaction/auth/test_as_handlers.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@
1616
import httpx2
1717
import pytest
1818
from inline_snapshot import snapshot
19-
from pydantic import AnyUrl
2019

2120
from mcp.server import Server
2221
from mcp.server.auth.provider import ProviderTokenVerifier
23-
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
22+
from mcp.shared.auth import OAuthClientInformationFull
2423
from tests.interaction._connect import mounted_app
2524
from tests.interaction._requirements import requirement
2625
from tests.interaction.auth._harness import REDIRECT_URI, auth_settings, oauth_client_metadata
@@ -331,25 +330,3 @@ async def test_a_non_loopback_http_redirect_uri_is_accepted_at_registration(
331330
info = OAuthClientInformationFull.model_validate_json(response.content)
332331
assert [str(u) for u in (info.redirect_uris or [])] == ["http://evil.example/callback"]
333332
assert info.client_id in provider.clients
334-
335-
336-
@requirement("hosting:auth:as:register-echo-application-type")
337-
async def test_register_echoes_native_for_a_client_that_registered_application_type_web(
338-
as_app: tuple[httpx2.AsyncClient, InMemoryAuthorizationServerProvider],
339-
) -> None:
340-
"""A client registering `application_type: "web"` is told `"native"` in the registration echo.
341-
342-
When the passthrough fix lands: re-pin the echo to `"web"` and delete the Divergence.
343-
"""
344-
http, _ = as_app
345-
metadata = OAuthClientMetadata(
346-
client_name="interaction-suite", redirect_uris=[AnyUrl(REDIRECT_URI)], application_type="web"
347-
)
348-
349-
response = await http.post("/register", content=metadata.model_dump_json())
350-
351-
assert response.status_code == 201
352-
body = response.json()
353-
assert body["application_type"] == "native"
354-
# The omission is specific to application_type, not a generally lossy echo.
355-
assert body["client_name"] == "interaction-suite"

tests/interaction/auth/test_flow.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,13 @@ async def test_dcr_sends_a_consumer_set_application_type_verbatim() -> None:
280280
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.
283+
The registration echo returns the same value, which the SDK adopts into the persisted
284+
client info, so a web client reads back web.
283285
"""
284286
requests: list[httpx2.Request] = []
285287
provider = InMemoryAuthorizationServerProvider()
286288
server = Server("guarded", on_list_tools=list_tools)
289+
storage = InMemoryTokenStorage()
287290
client_metadata = OAuthClientMetadata(
288291
client_name="interaction-suite",
289292
redirect_uris=[AnyUrl(REDIRECT_URI)],
@@ -292,14 +295,20 @@ async def test_dcr_sends_a_consumer_set_application_type_verbatim() -> None:
292295

293296
with anyio.fail_after(5):
294297
async with connect_with_oauth(
295-
server, provider=provider, client_metadata=client_metadata, on_request=requests.append
298+
server,
299+
provider=provider,
300+
storage=storage,
301+
client_metadata=client_metadata,
302+
on_request=requests.append,
296303
) as (client, _):
297304
result = await client.list_tools()
298305

299306
assert result.tools[0].name == "whoami"
300307

301308
register = next(r for r in requests if r.url.path == "/register")
302309
assert json.loads(register.content)["application_type"] == "web"
310+
assert storage.client_info is not None
311+
assert storage.client_info.application_type == "web"
303312

304313

305314
async def test_shimmed_app_serves_overrides_404s_and_otherwise_forwards_to_the_wrapped_app() -> None:

tests/interaction/lowlevel/test_cancellation.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ async def test_no_notifications_for_a_request_arrive_after_its_cancellation(conn
117117
async def collect(progress: float, total: float | None, message: str | None) -> None:
118118
progress_updates.append((progress, total, message))
119119

120+
outcomes: list[object] = []
121+
120122
async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult:
121123
assert params.name == "block"
122124
assert ctx.request_id is not None
@@ -141,23 +143,28 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
141143

142144
async with connect(server) as client:
143145
with anyio.fail_after(5):
144-
async with anyio.create_task_group() as task_group:
146+
async with anyio.create_task_group() as task_group: # pragma: no branch
145147

146-
async def call_and_swallow_cancellation_error() -> None:
147-
with pytest.raises(MCPError):
148-
await client.call_tool("block", {}, progress_callback=collect)
148+
async def await_doomed_call() -> None:
149+
try:
150+
outcomes.append(await client.call_tool("block", {}, progress_callback=collect))
151+
except MCPError as exc:
152+
outcomes.append(exc.error)
149153

150-
task_group.start_soon(call_and_swallow_cancellation_error)
154+
task_group.start_soon(await_doomed_call)
151155
await started.wait()
152156
await client.session.send_notification(
153157
types.CancelledNotification(
154158
params=types.CancelledNotificationParams(request_id=request_ids[0], reason="user aborted")
155159
)
156160
)
161+
await handler_cancelled.wait()
162+
# Let anything the server was going to send be delivered before checking.
163+
await anyio.wait_all_tasks_blocked()
164+
assert outcomes in ([], [_LEGACY_HTTP_TERMINATOR])
165+
task_group.cancel_scope.cancel() # abandon the call if it is still parked
157166

158-
await handler_cancelled.wait()
159-
160-
# Progress shares the ordered stream with the error response: a sent "too late" would already be here.
167+
# The "too late" send was itself cancelled before it could be written, so it never reached the wire.
161168
assert progress_updates == [(1.0, 2.0, "started")]
162169
assert attempted == ["send-cancelled"]
163170

0 commit comments

Comments
 (0)