Skip to content

Commit 34ab5e4

Browse files
committed
Modern results always carry the resultType discriminator
The 2026-07-28 schema requires Result.resultType: servers implementing the revision MUST include it; the absent-means-complete bridge exists for clients of older servers only. The per-version sieve guaranteed it for core spec methods but custom methods, extension methods, and empty results shipped without it, which the typescript client rejects as an invalid result. Fill 'complete' at the envelope pass when the field is absent (or explicitly null) on a modern-era result, mirroring the typescript server's stampResultType. Handshake-era results are untouched, and claimed extension shapes keep their own tag.
1 parent 3d5f673 commit 34ab5e4

3 files changed

Lines changed: 65 additions & 10 deletions

File tree

src/mcp/server/runner.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,13 @@ def _serialize(self, method: str, version: str, result: HandlerResult) -> dict[s
377377
# pydantic messages echo the result body.
378378
logger.exception("handler for %r returned an invalid result", method)
379379
raise MCPError(code=INTERNAL_ERROR, message="Handler returned an invalid result") from None
380+
if version in MODERN_PROTOCOL_VERSIONS and dumped.get("resultType") is None:
381+
# Spec 2026-07-28: `Result.resultType` is required - servers MUST
382+
# include it (the absent-means-complete bridge is for clients of
383+
# older servers only). The sieve guarantees it for core methods;
384+
# this covers everything else: custom methods, extension methods,
385+
# and empty results.
386+
dumped["resultType"] = "complete"
380387
return self._stamp_server_info(version, dumped)
381388

382389
def _stamp_server_info(self, version: str, result: dict[str, Any]) -> dict[str, Any]:

tests/server/test_runner.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -957,10 +957,12 @@ async def echo(ctx: Ctx, params: RequestParams) -> dict[str, Any]:
957957
born_ready = Connection.from_envelope(LATEST_MODERN_VERSION, None, None)
958958
async with connected_runner(server, initialized=False, connection=born_ready) as (client, _):
959959
result = await client.send_raw_request("myorg/echo", None)
960-
# Custom-method results served at a modern version carry the serverInfo
961-
# `_meta` stamp like any other result (spec 2026-07-28, #3002).
960+
# Custom-method results served at a modern version carry the required
961+
# `resultType` discriminator and the serverInfo `_meta` stamp like any
962+
# other result (spec 2026-07-28, #3002).
962963
assert result == {
963964
"echoed": True,
965+
"resultType": "complete",
964966
"_meta": {SERVER_INFO_META_KEY: {"name": "test-server", "version": "0.0.1"}},
965967
}
966968

@@ -1039,7 +1041,7 @@ async def custom(ctx: Ctx, params: RequestParams) -> dict[str, Any]:
10391041
born_ready = Connection.from_envelope(LATEST_MODERN_VERSION, None, None)
10401042
async with connected_runner(server, initialized=False, connection=born_ready) as (client, _):
10411043
result = await client.send_raw_request("myorg/odd-meta", None)
1042-
assert result == {"_meta": "not-a-mapping"}
1044+
assert result == {"_meta": "not-a-mapping", "resultType": "complete"}
10431045

10441046

10451047
@pytest.mark.anyio
@@ -1103,6 +1105,40 @@ async def custom(ctx: Ctx, params: CallToolRequestParams) -> dict[str, Any]:
11031105
assert exc.value.error.message == "Handler returned an invalid result"
11041106

11051107

1108+
@pytest.mark.anyio
1109+
async def test_a_handshake_era_custom_result_gains_no_discriminator(server: SrvT):
1110+
"""The `resultType` fill is modern-only: handshake-era results keep the
1111+
handler's exact shape (the field does not exist pre-2026)."""
1112+
1113+
async def echo(ctx: Ctx, params: RequestParams) -> dict[str, Any]:
1114+
return {"echoed": True}
1115+
1116+
server.add_request_handler("myorg/echo", RequestParams, echo)
1117+
async with connected_runner(server) as (client, _):
1118+
result = await client.send_raw_request("myorg/echo", None)
1119+
assert result == {"echoed": True}
1120+
1121+
1122+
@pytest.mark.anyio
1123+
async def test_an_explicit_null_result_type_is_filled_on_the_modern_path(server: SrvT):
1124+
"""A handler-authored `"resultType": null` reads as absent (null is not a
1125+
valid discriminator) and is filled, mirroring the null posture of the
1126+
identity stamp."""
1127+
1128+
async def custom(ctx: Ctx, params: RequestParams) -> dict[str, Any]:
1129+
return {"echoed": True, "resultType": None}
1130+
1131+
server.add_request_handler("myorg/echo", RequestParams, custom)
1132+
born_ready = Connection.from_envelope(LATEST_MODERN_VERSION, None, None)
1133+
async with connected_runner(server, initialized=False, connection=born_ready) as (client, _):
1134+
result = await client.send_raw_request("myorg/echo", None)
1135+
assert result == {
1136+
"echoed": True,
1137+
"resultType": "complete",
1138+
"_meta": {SERVER_INFO_META_KEY: {"name": "test-server", "version": "0.0.1"}},
1139+
}
1140+
1141+
11061142
@pytest.mark.anyio
11071143
async def test_a_claimed_extension_result_type_bypasses_the_sieve_and_is_stamped(server: SrvT):
11081144
"""SDK-defined: a spec-method result carrying an extension `resultType` is a
@@ -1125,10 +1161,11 @@ async def custom(ctx: Ctx, params: CallToolRequestParams) -> dict[str, Any]:
11251161

11261162

11271163
@pytest.mark.anyio
1128-
async def test_an_empty_result_on_the_modern_path_carries_only_the_stamp(server: SrvT):
1129-
"""Spec-mandated (2026-07-28, #3002): serverInfo is stamped into every
1130-
result, including empty ones — a result that dumps as `{}` goes on the
1131-
modern wire as just the identity `_meta`."""
1164+
async def test_an_empty_result_on_the_modern_path_carries_the_discriminator_and_stamp(server: SrvT):
1165+
"""Spec-mandated (2026-07-28): `resultType` is required on every result a
1166+
modern server sends (the absent-means-complete bridge is for clients of
1167+
older servers only), and serverInfo is stamped into every result too - so
1168+
a result that dumps as `{}` goes on the modern wire with both."""
11321169

11331170
async def custom(ctx: Ctx, params: RequestParams) -> EmptyResult:
11341171
return EmptyResult()
@@ -1137,7 +1174,10 @@ async def custom(ctx: Ctx, params: RequestParams) -> EmptyResult:
11371174
born_ready = Connection.from_envelope(LATEST_MODERN_VERSION, None, None)
11381175
async with connected_runner(server, initialized=False, connection=born_ready) as (client, _):
11391176
result = await client.send_raw_request("myorg/empty", None)
1140-
assert result == {"_meta": {SERVER_INFO_META_KEY: {"name": "test-server", "version": "0.0.1"}}}
1177+
assert result == {
1178+
"resultType": "complete",
1179+
"_meta": {SERVER_INFO_META_KEY: {"name": "test-server", "version": "0.0.1"}},
1180+
}
11411181

11421182

11431183
@pytest.mark.anyio
@@ -1879,7 +1919,11 @@ async def greet(ctx: Ctx, params: RequestParams | None) -> dict[str, Any]:
18791919
params["_meta"][CLIENT_INFO_META_KEY] = "not-an-object"
18801920
async with dual_era_client(greeter) as (client, _):
18811921
result = await client.send_raw_request("custom/greet", params)
1882-
assert result == {"ok": True, "_meta": {SERVER_INFO_META_KEY: {"name": "greeter-server", "version": "0.0.1"}}}
1922+
assert result == {
1923+
"ok": True,
1924+
"resultType": "complete",
1925+
"_meta": {SERVER_INFO_META_KEY: {"name": "greeter-server", "version": "0.0.1"}},
1926+
}
18831927
assert seen == [None]
18841928

18851929

tests/server/test_streamable_http_modern.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,11 @@ async def greet(ctx: ServerRequestContext, params: PaginatedRequestParams) -> di
215215
response = await http.post("/mcp", json=body, headers={MCP_METHOD_HEADER: "custom/greet"})
216216
assert response.status_code == 200
217217
result = response.json()["result"]
218-
assert result == {"_meta": {SERVER_INFO_META_KEY: {"name": "test", "version": ""}}, "ok": True}
218+
assert result == {
219+
"_meta": {SERVER_INFO_META_KEY: {"name": "test", "version": ""}},
220+
"ok": True,
221+
"resultType": "complete",
222+
}
219223
assert seen == [None]
220224

221225

0 commit comments

Comments
 (0)