Skip to content

Commit 7e32fc9

Browse files
author
vijay
committed
Test that request _meta is camelCase on the wire and snake_case to a handler
A caller's `_meta.progressToken` is carried over the wire under its camelCase JSON alias but deserialized back to the snake_case field `progress_token`, so a server handler reading `params.meta` finds `progress_token`. Pin both forms of the same params object for `skills/list` and `resources/directory/read`, and expand the client round-trip test's comment to spell out the distinction.
1 parent 63254f8 commit 7e32fc9

2 files changed

Lines changed: 26 additions & 1 deletion

File tree

tests/client/test_skills.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,9 @@ async def handler(ctx: ServerRequestContext[Any, Any], params: ListSkillsParams)
293293
async with Client(server) as client:
294294
params = ListSkillsParams.model_validate({"_meta": {"progressToken": "t"}})
295295
await list_skills(client.session, params)
296-
# The transport enriches `_meta` with its own keys; what matters is the caller's token
296+
# `_meta` crosses the wire camelCase (`progressToken`), but the server deserializes it back
297+
# through the meta model, which exposes the known field snake_case as `progress_token`; the
298+
# transport also enriches `_meta` with its own keys. What matters is the caller's token
297299
# reaching the server on both the first page and the cursor-following second one.
298300
assert len(seen_meta) == 2
299301
assert all(m is not None and m.get("progress_token") == "t" for m in seen_meta)

tests/shared/test_skills.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
"""
66

77
import hashlib
8+
from typing import Any
89

910
import pytest
1011
from mcp_types import Resource
1112

1213
from mcp.shared.skills import (
14+
ListSkillsParams,
1315
ListSkillsResult,
16+
ReadDirectoryParams,
1417
ReadDirectoryResult,
1518
Skill,
1619
SkillResource,
@@ -286,6 +289,26 @@ def test_validate_list_result_accepts_an_empty_listing() -> None:
286289
validate_list_result(ListSkillsResult(skills=[]))
287290

288291

292+
@pytest.mark.parametrize(
293+
("params_type", "payload"),
294+
[
295+
(ListSkillsParams, {"_meta": {"progressToken": "t"}}),
296+
(ReadDirectoryParams, {"uri": "skill://pdf/templates", "_meta": {"progressToken": "t"}}),
297+
],
298+
)
299+
def test_request_meta_is_camelcase_on_the_wire_but_snakecase_to_a_handler(
300+
params_type: type[ListSkillsParams] | type[ReadDirectoryParams], payload: dict[str, Any]
301+
) -> None:
302+
"""A caller's `_meta.progressToken` rides the wire camelCase (its JSON alias) yet is read back
303+
snake_case as `progress_token` once deserialized - two spellings, one value. This is what lets
304+
`list_skills`/`read_directory` forward a caller's `_meta` to every page while a server handler
305+
still finds `progress_token` in `params.meta`; both spellings working is the point."""
306+
params = params_type.model_validate(payload)
307+
wire = params.model_dump(by_alias=True, mode="json", exclude_none=True)
308+
assert wire["_meta"] == {"progressToken": "t"} # camelCase over the wire
309+
assert params.meta is not None and params.meta.get("progress_token") == "t" # snake_case to a handler
310+
311+
289312
@pytest.mark.parametrize("uri", ["skill://pdf/templates/", "not-a-uri"])
290313
def test_parse_directory_uri_rejects_malformed_uris(uri: str) -> None:
291314
with pytest.raises(ValueError, match="directory URI"):

0 commit comments

Comments
 (0)