Acknowledge notification POSTs with 202 on the 2026-07-28 HTTP entry (#3326)
This commit is contained in:
parent
9057285683
commit
b2025ab815
2 changed files with 254 additions and 37 deletions
|
|
@ -3182,7 +3182,12 @@ REQUIREMENTS: dict[str, Requirement] = {
|
|||
source=f"{SPEC_BASE_URL}/basic/transports#sending-messages-to-the-server",
|
||||
behavior="A POST containing only notifications or responses returns 202 with no body.",
|
||||
transports=("streamable-http",),
|
||||
note="Only observable over HTTP: 202 is an HTTP status code.",
|
||||
removed_in="2026-07-28",
|
||||
superseded_by="hosting:http:modern:notification-post-202",
|
||||
note=(
|
||||
"Only observable over HTTP: 202 is an HTTP status code. At 2026-07-28 clients no longer post "
|
||||
"responses (streamable-http §Sending Messages item 4), so only the notification half carries over."
|
||||
),
|
||||
),
|
||||
"hosting:http:onerror": Requirement(
|
||||
source="sdk",
|
||||
|
|
@ -3375,6 +3380,22 @@ REQUIREMENTS: dict[str, Requirement] = {
|
|||
transports=("streamable-http",),
|
||||
note="Only observable over streamable HTTP: the modern entry's JSONRPCError-to-HTTP-status mapping.",
|
||||
),
|
||||
"hosting:http:modern:notification-post-202": Requirement(
|
||||
source=f"{SPEC_2026_BASE_URL}/basic/transports/streamable-http#sending-messages",
|
||||
behavior=(
|
||||
"A 2026-07-28 POST whose body is a single JSON-RPC notification is acknowledged 202 with no "
|
||||
"body (the spec's accept branch) and is not dispatched; a posted JSON-RPC response is rejected "
|
||||
"INVALID_REQUEST at HTTP 400."
|
||||
),
|
||||
added_in="2026-07-28",
|
||||
supersedes=("hosting:http:notifications-202",),
|
||||
transports=("streamable-http",),
|
||||
note=(
|
||||
"Only observable over streamable HTTP: the HTTP status is the assertion. The revision defines no "
|
||||
"client-to-server notifications on this transport (cancellation is closing the response stream), "
|
||||
"so accept-and-drop is the SDK's choice between the two responses the spec permits."
|
||||
),
|
||||
),
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Client transport: streamable HTTP
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from mcp_types import (
|
|||
HEADER_MISMATCH,
|
||||
INTERNAL_ERROR,
|
||||
INVALID_PARAMS,
|
||||
INVALID_REQUEST,
|
||||
METHOD_NOT_FOUND,
|
||||
MISSING_REQUIRED_CLIENT_CAPABILITY,
|
||||
SERVER_INFO_META_KEY,
|
||||
|
|
@ -27,6 +28,7 @@ from mcp_types import (
|
|||
CallToolResult,
|
||||
DiscoverResult,
|
||||
EmptyResult,
|
||||
ErrorData,
|
||||
Implementation,
|
||||
JSONRPCError,
|
||||
JSONRPCResponse,
|
||||
|
|
@ -153,6 +155,39 @@ async def test_modern_response_carries_no_session_id_header() -> None:
|
|||
assert "mcp-session-id" not in response.headers
|
||||
|
||||
|
||||
@requirement("hosting:http:modern:notification-post-202")
|
||||
@pytest.mark.parametrize("json_response", [True, False], ids=["json", "sse"])
|
||||
@pytest.mark.parametrize("stateless_http", [True, False], ids=["stateless-flag", "default"])
|
||||
async def test_modern_notification_post_is_acknowledged_202_and_a_posted_response_is_rejected(
|
||||
json_response: bool, stateless_http: bool
|
||||
) -> None:
|
||||
"""A 2026-07-28 notification POST is answered 202 with no body; a posted response is 400 INVALID_REQUEST.
|
||||
|
||||
Spec-permitted (streamable-http §Sending Messages item 5): the server may accept (202) or refuse
|
||||
(4xx) a notification POST, and the SDK accepts -- the same answer the legacy leg gives, so a
|
||||
client's courtesy `notifications/cancelled` is not met with an error on one era only.
|
||||
Spec-mandated (item 4): clients MUST NOT post responses, so one is refused. Driven through the
|
||||
mounted app so the manager's header routing is in the path, under both response modes and both
|
||||
values of the legacy-only `stateless_http` flag (neither is read before the modern entry answers).
|
||||
"""
|
||||
notification = {"jsonrpc": "2.0", "method": "notifications/cancelled", "params": {"requestId": 1}}
|
||||
posted_response: dict[str, Any] = {"jsonrpc": "2.0", "id": 1, "result": {}}
|
||||
async with mounted_app(_server(), json_response=json_response, stateless_http=stateless_http) as (http, _):
|
||||
acknowledged = await http.post(
|
||||
"/mcp", json=notification, headers=_modern_headers(method="notifications/cancelled")
|
||||
)
|
||||
refused = await http.post("/mcp", json=posted_response, headers=_modern_headers(method="tools/list"))
|
||||
|
||||
assert (acknowledged.status_code, acknowledged.content) == (202, b"")
|
||||
assert "mcp-session-id" not in acknowledged.headers
|
||||
assert refused.status_code == 400
|
||||
assert JSONRPCError.model_validate(refused.json()) == JSONRPCError(
|
||||
jsonrpc="2.0",
|
||||
id=None,
|
||||
error=ErrorData(code=INVALID_REQUEST, message="Body must be a single JSON-RPC request or notification object"),
|
||||
)
|
||||
|
||||
|
||||
@requirement("hosting:http:modern:initialize-removed")
|
||||
async def test_modern_initialize_is_method_not_found() -> None:
|
||||
"""A 2026-07-28 initialize request that carries a valid envelope is answered METHOD_NOT_FOUND at HTTP 404.
|
||||
|
|
|
|||
Loading…
Reference in a new issue