Skip to content

Commit 01f6a63

Browse files
committed
test: cover sampling, client output-schema, and mcpserver gap requirements
1 parent c13d6ae commit 01f6a63

9 files changed

Lines changed: 709 additions & 39 deletions

File tree

src/mcp/client/session.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,7 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
337337
from jsonschema import SchemaError, ValidationError, validate
338338

339339
if result.structured_content is None:
340-
raise RuntimeError(
341-
f"Tool {name} has an output schema but did not return structured content"
342-
) # pragma: no cover
340+
raise RuntimeError(f"Tool {name} has an output schema but did not return structured content")
343341
try:
344342
validate(result.structured_content, output_schema)
345343
except ValidationError as e:

src/mcp/server/mcpserver/prompts/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,5 +185,5 @@ async def render(
185185
raise ValueError(f"Could not convert prompt result to message: {msg}")
186186

187187
return messages
188-
except Exception as e: # pragma: no cover
188+
except Exception as e:
189189
raise ValueError(f"Error rendering prompt {self.name}: {e}")

tests/interaction/_requirements.py

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -633,10 +633,6 @@ def __post_init__(self) -> None:
633633
"client:output-schema:skip-on-error": Requirement(
634634
source="sdk",
635635
behavior="The client skips structured-content validation when the tool result has isError true.",
636-
deferred=(
637-
"Not yet covered here: planned gap test (an isError result with mismatching structuredContent "
638-
"is returned to the caller rather than rejected)."
639-
),
640636
),
641637
"client:output-schema:validate": Requirement(
642638
source=f"{SPEC_BASE_URL}/server/tools#output-schema",
@@ -645,28 +641,49 @@ def __post_init__(self) -> None:
645641
"is rejected by the client: the call raises instead of returning the invalid result."
646642
),
647643
),
644+
"client:output-schema:missing-structured": Requirement(
645+
source="sdk",
646+
behavior="A tool that declares an output schema but returns no structuredContent fails client-side validation.",
647+
),
648+
"client:output-schema:auto-list": Requirement(
649+
source="sdk",
650+
behavior=(
651+
"Calling a tool whose output schema is not yet cached issues an implicit tools/list to "
652+
"populate the cache; subsequent calls of the same tool do not."
653+
),
654+
divergence=Divergence(
655+
note=(
656+
"Design concern rather than spec violation: the implicit request is invisible to the "
657+
"caller, and against a server that registers only on_call_tool a successful call surfaces "
658+
"as METHOD_NOT_FOUND from a tools/list the caller never asked for."
659+
),
660+
),
661+
),
648662
"mcpserver:output-schema:missing-structured": Requirement(
649663
source=f"{SPEC_BASE_URL}/server/tools#output-schema",
650664
behavior="A tool with an output schema whose function returns no structured content produces a server error.",
651-
deferred="Not yet covered here: planned gap test (output schema declared but no structured content returned).",
652665
),
653666
"mcpserver:output-schema:server-validate": Requirement(
654667
source=f"{SPEC_BASE_URL}/server/tools#output-schema",
655668
behavior=(
656669
"MCPServer validates structured content against the tool's output schema before returning; a "
657670
"mismatch produces a server error."
658671
),
659-
deferred="Not yet covered here: planned gap test (server-side output schema validation failure).",
660672
),
661673
"mcpserver:output-schema:skip-on-error": Requirement(
662674
source="sdk",
663675
behavior="Server-side output schema validation is skipped when the tool returns an isError result.",
664-
deferred="Not yet covered here: planned gap test (isError results bypass server-side schema validation).",
665676
),
666677
"mcpserver:tool:duplicate-name": Requirement(
667678
source=f"{SPEC_BASE_URL}/server/tools#tool-names",
668679
behavior="Registering a tool with a name already in use is rejected at registration time.",
669-
deferred="Not yet covered here: planned gap test (duplicate tool registration).",
680+
divergence=Divergence(
681+
note=(
682+
"MCPServer logs a warning and keeps the first registration instead of rejecting; "
683+
"warn_on_duplicate_tools defaults to True and warning is the only effect -- there is "
684+
"no rejection mode."
685+
),
686+
),
670687
),
671688
"mcpserver:tool:extra": Requirement(
672689
source="sdk",
@@ -693,7 +710,10 @@ def __post_init__(self) -> None:
693710
"mcpserver:tool:naming-validation": Requirement(
694711
source="sdk",
695712
behavior="Tool names that violate the spec's naming rules are rejected at registration time.",
696-
deferred="Not yet covered here: tool-name validation at registration has not been pinned yet.",
713+
deferred=(
714+
"Not implemented in the SDK: MCPServer accepts any string as a tool name; there is no "
715+
"spec-naming-rules check at registration time."
716+
),
697717
),
698718
"mcpserver:tool:output-schema:model": Requirement(
699719
source="sdk",
@@ -737,10 +757,6 @@ def __post_init__(self) -> None:
737757
"A tool function that raises the URL-elicitation-required error surfaces to the caller as "
738758
"error -32042 with the elicitation parameters intact."
739759
),
740-
deferred=(
741-
"Not yet covered here: the low-level equivalent is pinned by elicitation:url:required-error; "
742-
"the MCPServer-decorated path is a planned gap test."
743-
),
744760
),
745761
# ═══════════════════════════════════════════════════════════════════════════
746762
# MCPServer: Context helpers (SDK)
@@ -882,12 +898,20 @@ def __post_init__(self) -> None:
882898
"mcpserver:resource:duplicate-name": Requirement(
883899
source="sdk",
884900
behavior="Registering a resource or template with a duplicate identifier is rejected at registration time.",
885-
deferred="Not yet covered here: planned gap test (duplicate resource registration).",
901+
divergence=Divergence(
902+
note=(
903+
"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)."
905+
),
906+
),
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+
),
886911
),
887912
"mcpserver:resource:read-throws-surfaced": Requirement(
888913
source="sdk",
889914
behavior="A resource function that raises is surfaced to the caller as a JSON-RPC error response.",
890-
deferred="Not yet covered here: planned gap test (resource function raising during read).",
891915
),
892916
"mcpserver:resource:static": Requirement(
893917
source="sdk",
@@ -983,7 +1007,6 @@ def __post_init__(self) -> None:
9831007
"mcpserver:prompt:args-validation": Requirement(
9841008
source=f"{SPEC_BASE_URL}/server/prompts#implementation-considerations",
9851009
behavior="prompts/get arguments that fail the prompt's argument schema are rejected before the function runs.",
986-
deferred="Not yet covered here: planned gap test (argument validation on decorated prompts).",
9871010
),
9881011
"mcpserver:prompt:decorated": Requirement(
9891012
source="sdk",
@@ -995,12 +1018,20 @@ def __post_init__(self) -> None:
9951018
"mcpserver:prompt:duplicate-name": Requirement(
9961019
source="sdk",
9971020
behavior="Registering a duplicate prompt name is rejected at registration time.",
998-
deferred="Not yet covered here: planned gap test (duplicate prompt registration).",
1021+
divergence=Divergence(
1022+
note=(
1023+
"MCPServer logs a warning and keeps the first registration instead of rejecting; same "
1024+
"warn-and-ignore behaviour as duplicate tool names (mcpserver:tool:duplicate-name)."
1025+
),
1026+
),
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+
),
9991031
),
10001032
"mcpserver:prompt:optional-args": Requirement(
10011033
source="sdk",
10021034
behavior="A prompt with optional arguments can be fetched without supplying them.",
1003-
deferred="Not yet covered here: planned gap test (optional prompt arguments omitted).",
10041035
),
10051036
"mcpserver:prompt:unknown-name": Requirement(
10061037
source=f"{SPEC_BASE_URL}/server/prompts#error-handling",
@@ -1056,7 +1087,6 @@ def __post_init__(self) -> None:
10561087
"MCPServer advertises the completions capability when at least one completion source is "
10571088
"registered, and omits it otherwise."
10581089
),
1059-
deferred="Not yet covered here: planned gap test (automatic completions capability derivation).",
10601090
),
10611091
# ═══════════════════════════════════════════════════════════════════════════
10621092
# Logging
@@ -1112,7 +1142,6 @@ def __post_init__(self) -> None:
11121142
behavior=(
11131143
"A client that handles sampling requests advertises the sampling capability in its initialize request."
11141144
),
1115-
deferred="Not yet covered here: planned gap test (positive sampling capability declaration).",
11161145
),
11171146
"sampling:create:basic": Requirement(
11181147
source=f"{SPEC_BASE_URL}/client/sampling#creating-messages",
@@ -1137,10 +1166,6 @@ def __post_init__(self) -> None:
11371166
"capability; the server-side validator only checks tools/tool_choice."
11381167
),
11391168
),
1140-
deferred=(
1141-
"Not implemented in the SDK: include_context is forwarded regardless of the client's declared "
1142-
"sampling.context capability (unlike tools, which are gated by the server-side validator)."
1143-
),
11441169
),
11451170
"sampling:create:model-preferences": Requirement(
11461171
source=f"{SPEC_BASE_URL}/client/sampling#model-preferences",
@@ -1168,7 +1193,6 @@ def __post_init__(self) -> None:
11681193
"sampling:create-message:audio-content": Requirement(
11691194
source=f"{SPEC_BASE_URL}/client/sampling#audio-content",
11701195
behavior="Sampling messages can carry audio content: base64 data with a mimeType.",
1171-
deferred="Not yet covered here: planned gap test (audio content in sampling messages, both directions).",
11721196
),
11731197
"sampling:create-message:image-content": Requirement(
11741198
source=f"{SPEC_BASE_URL}/client/sampling#image-content",
@@ -1191,15 +1215,20 @@ def __post_init__(self) -> None:
11911215
"sampling:message:content-cardinality": Requirement(
11921216
source=f"{SPEC_BASE_URL}/client/sampling",
11931217
behavior="A sampling message's content may be a single block or an array of blocks.",
1194-
deferred="Not yet covered here: planned gap test (list-valued sampling message content).",
11951218
),
11961219
"sampling:result:no-tools-single-content": Requirement(
11971220
source="sdk",
11981221
behavior=(
11991222
"When the request carries no tools, a sampling callback result whose content is an array is "
12001223
"rejected by the client."
12011224
),
1202-
deferred="Not yet covered here: planned gap test (array content rejected for tool-free sampling).",
1225+
divergence=Divergence(
1226+
note=(
1227+
"The client does not validate the callback result against the request shape; an array-content "
1228+
"result for a tool-free request is accepted client-side and surfaces as a raw "
1229+
"pydantic.ValidationError from the server's response parsing (send_request) instead."
1230+
),
1231+
),
12031232
),
12041233
"sampling:result:with-tools-array-content": Requirement(
12051234
source="sdk",
@@ -1225,7 +1254,6 @@ def __post_init__(self) -> None:
12251254
"Every assistant tool_use block in a sampling request must be matched by a tool_result with "
12261255
"the same id in the following user message; an unmatched tool_use is rejected with Invalid params."
12271256
),
1228-
deferred="Not yet covered here: planned gap test (unmatched tool_use rejected by the validator).",
12291257
),
12301258
"sampling:tools:server-gated-by-capability": Requirement(
12311259
source=f"{SPEC_BASE_URL}/client/sampling#tools-in-sampling",
@@ -1433,9 +1461,10 @@ def __post_init__(self) -> None:
14331461
"of roots changes."
14341462
),
14351463
deferred=(
1436-
"Not implemented in the SDK: the client keeps no managed roots store, so nothing fires "
1437-
"automatically when the configured roots change; emission is an explicit "
1438-
"send_roots_list_changed() call (pinned by roots:list-changed)."
1464+
"Not implemented in the SDK: the client does not own the root set (it calls back to the host "
1465+
"via list_roots_callback), so there is no mutation it could observe to auto-emit on; the SDK "
1466+
"provides send_roots_list_changed() for the host to call when its roots change, and that "
1467+
"emission path is covered by roots:list-changed."
14391468
),
14401469
),
14411470
"roots:list:basic": Requirement(
@@ -1467,8 +1496,9 @@ def __post_init__(self) -> None:
14671496
source=f"{SPEC_BASE_URL}/client/roots#root",
14681497
behavior="Every root returned by the client identifies itself with a file:// URI.",
14691498
deferred=(
1470-
"Not yet covered here: planned gap test (the SDK's Root type enforces the file:// scheme; pin "
1471-
"it end-to-end through roots/list)."
1499+
"Schema-level validation: the FileUrl type on Root.uri rejects any non-file:// scheme at "
1500+
"construction and at parse, so a non-conforming root cannot reach the wire from either side; "
1501+
"type-level coverage belongs in tests/test_types.py rather than this interaction suite."
14721502
),
14731503
),
14741504
# ═══════════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)