Skip to content

Commit 1e0d4f6

Browse files
committed
test: cover server-feature, pagination, elicitation, and mcpserver gap requirements
1 parent 01f6a63 commit 1e0d4f6

10 files changed

Lines changed: 661 additions & 67 deletions

File tree

tests/interaction/_requirements.py

Lines changed: 7 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,8 @@ def __post_init__(self) -> None:
333333
"direction and are believed to still be in flight."
334334
),
335335
deferred=(
336-
"Not yet covered here: there is no public client-side cancel API to drive (see "
337-
"protocol:cancel:abort-signal), so the sender-side targeting rule has nothing to pin yet."
336+
"Not implemented in the SDK: there is no public client-side cancel API to drive (see "
337+
"protocol:cancel:abort-signal), so the sender-side targeting rule has nothing to pin."
338338
),
339339
),
340340
"protocol:error:connection-closed": Requirement(
@@ -573,34 +573,18 @@ def __post_init__(self) -> None:
573573
"A tool registered with a JSON Schema 2020-12 inputSchema (nested objects, $defs references) "
574574
"is discoverable and callable."
575575
),
576-
deferred=(
577-
"Not yet covered here; existing coverage in tests/test_types.py at the type level; an "
578-
"interaction-level passthrough test is planned with the gap batch."
579-
),
580576
),
581577
"tools:input-schema:preserve-additional-properties": Requirement(
582578
source=f"{SPEC_BASE_URL}/server/tools#tool",
583579
behavior="tools/list preserves inputSchema additionalProperties as registered.",
584-
deferred=(
585-
"Not yet covered here; existing coverage in tests/test_types.py at the type level; an "
586-
"interaction-level passthrough test is planned with the gap batch."
587-
),
588580
),
589581
"tools:input-schema:preserve-defs": Requirement(
590582
source=f"{SPEC_BASE_URL}/server/tools#tool",
591583
behavior="tools/list preserves inputSchema $defs as registered.",
592-
deferred=(
593-
"Not yet covered here; existing coverage in tests/test_types.py at the type level; an "
594-
"interaction-level passthrough test is planned with the gap batch."
595-
),
596584
),
597585
"tools:input-schema:preserve-schema-dialect": Requirement(
598586
source=f"{SPEC_BASE_URL}/server/tools#tool",
599587
behavior="tools/list preserves the inputSchema $schema dialect URI as registered.",
600-
deferred=(
601-
"Not yet covered here; existing coverage in tests/test_types.py at the type level; an "
602-
"interaction-level passthrough test is planned with the gap batch."
603-
),
604588
),
605589
"tools:list-changed": Requirement(
606590
source=f"{SPEC_BASE_URL}/server/tools#list-changed-notification",
@@ -791,10 +775,9 @@ def __post_init__(self) -> None:
791775
"resources:annotations": Requirement(
792776
source=f"{SPEC_BASE_URL}/server/resources#annotations",
793777
behavior=(
794-
"Resource annotations (audience, priority, lastModified) supplied by the server round-trip to "
795-
"the client in list and read results."
778+
"Resource annotations (audience, priority) supplied by the server round-trip to the client "
779+
"in the list result."
796780
),
797-
deferred="Not yet covered here: planned gap test (annotations passthrough on list and read results).",
798781
),
799782
"resources:capability:declared": Requirement(
800783
source=f"{SPEC_BASE_URL}/server/resources#capabilities",
@@ -846,10 +829,6 @@ def __post_init__(self) -> None:
846829
behavior=(
847830
"resources/subscribe to a server that did not advertise the subscribe capability is rejected with an error."
848831
),
849-
deferred=(
850-
"Not yet covered here: planned gap test (subscribe rejected with METHOD_NOT_FOUND when no "
851-
"subscribe handler is registered)."
852-
),
853832
),
854833
"resources:subscribe:updated": Requirement(
855834
source=f"{SPEC_BASE_URL}/server/resources#subscriptions",
@@ -901,13 +880,10 @@ def __post_init__(self) -> None:
901880
divergence=Divergence(
902881
note=(
903882
"MCPServer logs a warning and keeps the first registration instead of rejecting; same "
904-
"warn-and-ignore behaviour as duplicate tool names (mcpserver:tool:duplicate-name)."
883+
"warn-and-ignore behaviour as duplicate tool names (mcpserver:tool:duplicate-name). "
884+
"Templates differ: a duplicate uri_template silently replaces the first with no warning."
905885
),
906886
),
907-
deferred=(
908-
"Not yet covered here: mechanical sibling of mcpserver:tool:duplicate-name (same "
909-
"warn-and-ignore behaviour); planned as a small follow-on to that test."
910-
),
911887
),
912888
"mcpserver:resource:read-throws-surfaced": Requirement(
913889
source="sdk",
@@ -947,17 +923,14 @@ def __post_init__(self) -> None:
947923
"prompts:get:content:audio": Requirement(
948924
source=f"{SPEC_BASE_URL}/server/prompts#audio-content",
949925
behavior="Prompt messages may contain audio content with base64 data and a mimeType.",
950-
deferred="Not yet covered here: planned gap test (audio content in prompt messages).",
951926
),
952927
"prompts:get:content:embedded-resource": Requirement(
953928
source=f"{SPEC_BASE_URL}/server/prompts#embedded-resources",
954929
behavior="Prompt messages may contain embedded resource content.",
955-
deferred="Not yet covered here: planned gap test (embedded resources in prompt messages).",
956930
),
957931
"prompts:get:content:image": Requirement(
958932
source=f"{SPEC_BASE_URL}/server/prompts#image-content",
959933
behavior="Prompt messages may contain image content.",
960-
deferred="Not yet covered here: planned gap test (image content in prompt messages).",
961934
),
962935
"prompts:get:missing-required-args": Requirement(
963936
source=f"{SPEC_BASE_URL}/server/prompts#error-handling",
@@ -976,7 +949,6 @@ def __post_init__(self) -> None:
976949
"prompts:get:no-args": Requirement(
977950
source=f"{SPEC_BASE_URL}/server/prompts#getting-a-prompt",
978951
behavior="prompts/get with no arguments returns the prompt's messages.",
979-
deferred="Not yet covered here: planned gap test (argument-free prompt fetched without arguments).",
980952
),
981953
"prompts:get:unknown-name": Requirement(
982954
source=f"{SPEC_BASE_URL}/server/prompts#error-handling",
@@ -1024,10 +996,6 @@ def __post_init__(self) -> None:
1024996
"warn-and-ignore behaviour as duplicate tool names (mcpserver:tool:duplicate-name)."
1025997
),
1026998
),
1027-
deferred=(
1028-
"Not yet covered here: mechanical sibling of mcpserver:tool:duplicate-name (same "
1029-
"warn-and-ignore behaviour); planned as a small follow-on to that test."
1030-
),
1031999
),
10321000
"mcpserver:prompt:optional-args": Requirement(
10331001
source="sdk",
@@ -1067,7 +1035,6 @@ def __post_init__(self) -> None:
10671035
"completion/complete with a ref naming an unknown prompt or non-matching resource URI returns "
10681036
"JSON-RPC error -32602 (Invalid params)."
10691037
),
1070-
deferred="Not yet covered here: planned gap test (completion against an unknown ref).",
10711038
),
10721039
"completion:prompt-arg": Requirement(
10731040
source=f"{SPEC_BASE_URL}/server/utilities/completion#reference-types",
@@ -1132,7 +1099,6 @@ def __post_init__(self) -> None:
11321099
"logging:set-level:invalid-level": Requirement(
11331100
source=f"{SPEC_BASE_URL}/server/utilities/logging#error-handling",
11341101
behavior="logging/setLevel with an invalid level value returns JSON-RPC error -32602 (Invalid params).",
1135-
deferred="Not yet covered here: planned gap test (invalid level value on setLevel).",
11361102
),
11371103
# ═══════════════════════════════════════════════════════════════════════════
11381104
# Sampling (server → client)
@@ -1294,13 +1260,9 @@ def __post_init__(self) -> None:
12941260
divergence=Divergence(
12951261
note=(
12961262
"The server does not check the client's declared elicitation modes before sending "
1297-
"elicitation/create; the spec's SHOULD is not enforced."
1263+
"elicitation/create; the spec's MUST NOT is not enforced."
12981264
),
12991265
),
1300-
deferred=(
1301-
"Not implemented in the SDK: the server does not check the client's declared elicitation "
1302-
"modes before sending elicitation/create."
1303-
),
13041266
),
13051267
"elicitation:form:action:accept": Requirement(
13061268
source=f"{SPEC_BASE_URL}/client/elicitation#response-actions",
@@ -1338,7 +1300,6 @@ def __post_init__(self) -> None:
13381300
"elicitation:form:mode-omitted-default": Requirement(
13391301
source=f"{SPEC_BASE_URL}/client/elicitation#elicitation-requests",
13401302
behavior="An elicitation request with no mode field is treated as form mode by the client.",
1341-
deferred="Not yet covered here: planned gap test (mode-less elicitation request handled as form mode).",
13421303
),
13431304
"elicitation:form:not-supported": Requirement(
13441305
source=f"{SPEC_BASE_URL}/client/elicitation#error-handling",
@@ -1356,12 +1317,10 @@ def __post_init__(self) -> None:
13561317
"Requested-schema enum fields (including titled and multi-select variants) reach the client "
13571318
"callback as sent."
13581319
),
1359-
deferred="Not yet covered here: planned gap test (enum variants in the requested schema).",
13601320
),
13611321
"elicitation:form:schema:primitives": Requirement(
13621322
source=f"{SPEC_BASE_URL}/client/elicitation#requested-schema",
13631323
behavior="Requested-schema fields may be string (with format), number or integer, or boolean.",
1364-
deferred="Not yet covered here: planned gap test (full primitive-type coverage in the requested schema).",
13651324
),
13661325
"elicitation:form:schema:restricted-subset": Requirement(
13671326
source=f"{SPEC_BASE_URL}/client/elicitation#requested-schema",
@@ -1375,10 +1334,6 @@ def __post_init__(self) -> None:
13751334
"can send nested or non-primitive schemas and the SDK forwards them unchanged."
13761335
),
13771336
),
1378-
deferred=(
1379-
"Not implemented in the SDK: nothing restricts or validates the requested-schema shape on the "
1380-
"sending side; hand-built lowlevel elicitation requests pass through unchecked."
1381-
),
13821337
),
13831338
"elicitation:form:response-validation": Requirement(
13841339
source=f"{SPEC_BASE_URL}/client/elicitation#form-mode-security",
@@ -1389,7 +1344,6 @@ def __post_init__(self) -> None:
13891344
divergence=Divergence(
13901345
note="Accepted elicitation content passes through unvalidated on both sides.",
13911346
),
1392-
deferred=("Not implemented in the SDK: accepted elicitation content passes through unvalidated on both sides."),
13931347
),
13941348
"elicitation:url:action:accept-no-content": Requirement(
13951349
source=f"{SPEC_BASE_URL}/client/elicitation#response-actions",
@@ -1423,7 +1377,6 @@ def __post_init__(self) -> None:
14231377
"The client ignores an elicitation/complete notification referencing an unknown or "
14241378
"already-completed elicitationId without error."
14251379
),
1426-
deferred="Not yet covered here: planned gap test (unknown elicitationId in a complete notification).",
14271380
),
14281381
"elicitation:url:decline": Requirement(
14291382
source=f"{SPEC_BASE_URL}/client/elicitation#response-actions",
@@ -1566,18 +1519,13 @@ def __post_init__(self) -> None:
15661519
"pagination:invalid-cursor": Requirement(
15671520
source=f"{SPEC_BASE_URL}/server/utilities/pagination#error-handling",
15681521
behavior="A list request with an invalid cursor returns JSON-RPC error -32602 (Invalid params).",
1569-
deferred="Not yet covered here: planned gap test (invalid pagination cursor rejected).",
15701522
),
15711523
"pagination:client:cursor-handling": Requirement(
15721524
source=f"{SPEC_BASE_URL}/server/utilities/pagination#implementation-guidelines",
15731525
behavior=(
15741526
"The client treats cursors as opaque tokens — it does not parse, modify, or persist them — "
15751527
"and does not assume a fixed page size."
15761528
),
1577-
deferred=(
1578-
"Not yet covered here: planned gap test (the client passes a server-issued cursor back "
1579-
"byte-for-byte and follows pages of varying sizes)."
1580-
),
15811529
),
15821530
# ═══════════════════════════════════════════════════════════════════════════
15831531
# Tasks (experimental)

tests/interaction/lowlevel/test_completion.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from mcp import MCPError, types
77
from mcp.server import Server, ServerRequestContext
88
from mcp.types import (
9+
INVALID_PARAMS,
910
METHOD_NOT_FOUND,
1011
CompleteResult,
1112
Completion,
@@ -93,6 +94,28 @@ async def completion(ctx: ServerRequestContext, params: types.CompleteRequestPar
9394
assert result == snapshot(CompleteResult(completion=Completion(values=["modelcontextprotocol/python-sdk"])))
9495

9596

97+
@requirement("completion:error:invalid-ref")
98+
async def test_completion_against_an_unknown_ref_is_rejected_with_invalid_params(connect: Connect) -> None:
99+
"""completion/complete with a ref naming an unknown prompt is answered with -32602 Invalid params.
100+
101+
The lowlevel server does not validate refs itself (it has no prompt/template registry to check
102+
against); rejecting an unknown ref is the handler's job, and this test pins the spec-recommended
103+
way to do it.
104+
"""
105+
106+
async def completion(ctx: ServerRequestContext, params: types.CompleteRequestParams) -> CompleteResult:
107+
assert isinstance(params.ref, PromptReference)
108+
raise MCPError(code=INVALID_PARAMS, message=f"Unknown prompt: {params.ref.name!r}")
109+
110+
server = Server("completer", on_completion=completion)
111+
112+
async with connect(server) as client:
113+
with pytest.raises(MCPError) as exc_info:
114+
await client.complete(PromptReference(name="ghost"), argument={"name": "x", "value": ""})
115+
116+
assert exc_info.value.error.code == INVALID_PARAMS
117+
118+
96119
@requirement("completion:complete:not-supported")
97120
@requirement("protocol:error:method-not-found")
98121
async def test_complete_without_handler_is_method_not_found(connect: Connect) -> None:

0 commit comments

Comments
 (0)