Files
wehub-resource-sync 49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

196 lines
7.2 KiB
Python

"""Prompt interactions against MCPServer, driven through the public Client API."""
import pytest
from inline_snapshot import snapshot
from mcp_types import (
ErrorData,
GetPromptResult,
ListPromptsResult,
Prompt,
PromptArgument,
PromptMessage,
TextContent,
)
from mcp import MCPError
from mcp.server.mcpserver import MCPServer
from tests.interaction._connect import Connect
from tests.interaction._requirements import requirement
pytestmark = pytest.mark.anyio
@requirement("mcpserver:prompt:decorated")
async def test_list_prompts_derives_arguments_from_signature(connect: Connect) -> None:
"""A decorated prompt is listed with arguments derived from the function signature.
Parameters without a default are required; the description comes from the docstring.
"""
mcp = MCPServer("prompter")
@mcp.prompt()
def code_review(code: str, style_guide: str = "pep8") -> str:
"""Review a piece of code."""
raise NotImplementedError # registered for listing only; never rendered
async with connect(mcp) as client:
result = await client.list_prompts()
assert result == snapshot(
ListPromptsResult(
prompts=[
Prompt(
name="code_review",
description="Review a piece of code.",
arguments=[
PromptArgument(name="code", required=True),
PromptArgument(name="style_guide", required=False),
],
)
]
)
)
@requirement("mcpserver:prompt:decorated")
async def test_get_prompt_renders_function_return(connect: Connect) -> None:
"""The decorated function's string return value is rendered as a single user message."""
mcp = MCPServer("prompter")
@mcp.prompt()
def greet(name: str) -> str:
"""A personalised greeting."""
return f"Say hello to {name}."
async with connect(mcp) as client:
result = await client.get_prompt("greet", {"name": "Ada"})
assert result == snapshot(
GetPromptResult(
description="A personalised greeting.",
messages=[PromptMessage(role="user", content=TextContent(text="Say hello to Ada."))],
)
)
@requirement("mcpserver:prompt:unknown-name")
async def test_get_unknown_prompt_is_error(connect: Connect) -> None:
"""Getting a prompt name that was never registered fails with a JSON-RPC error.
The spec reserves -32602 for this case; the SDK reports code 0 (see the divergence note on
the requirement).
"""
mcp = MCPServer("prompter")
@mcp.prompt()
def greet(name: str) -> str:
"""A registered prompt; the test requests a different name."""
raise NotImplementedError
async with connect(mcp) as client:
with pytest.raises(MCPError) as exc_info:
await client.get_prompt("nope")
assert exc_info.value.error == snapshot(ErrorData(code=0, message="Unknown prompt: nope"))
@requirement("prompts:get:missing-required-args")
async def test_get_prompt_with_a_missing_required_argument_is_an_error(connect: Connect) -> None:
"""Getting a prompt without one of its required arguments fails with a JSON-RPC error.
The missing argument is detected before the prompt function is called, but the spec's -32602
Invalid params is reported as error code 0 with the bare exception text (see the divergence
note on the requirement).
"""
mcp = MCPServer("prompter")
@mcp.prompt()
def greet(name: str) -> str:
"""A registered prompt; validation rejects the call before the function runs."""
raise NotImplementedError
async with connect(mcp) as client:
with pytest.raises(MCPError) as exc_info:
await client.get_prompt("greet")
assert exc_info.value.error == snapshot(ErrorData(code=0, message="Missing required arguments: {'name'}"))
@requirement("mcpserver:prompt:args-validation")
async def test_get_prompt_with_a_wrong_type_argument_is_rejected_before_the_function_runs(connect: Connect) -> None:
"""An argument that fails the function signature's type validation is rejected before the function runs.
The decorated function is wrapped in pydantic's validate_call, so a value that cannot be
coerced to the parameter's annotation fails before the body executes. The function body
raises NotImplementedError to prove it never ran. The error is wrapped in the SDK's stable
rendering-error prefix; the body of the message is raw pydantic output and is not asserted.
"""
mcp = MCPServer("prompter")
@mcp.prompt()
def repeat(phrase: str, count: int) -> str:
"""A registered prompt; type validation rejects the call before the function runs."""
raise NotImplementedError
async with connect(mcp) as client:
with pytest.raises(MCPError) as exc_info:
await client.get_prompt("repeat", {"phrase": "hi", "count": "many"})
assert exc_info.value.error.code == 0
assert exc_info.value.error.message.startswith("Error rendering prompt repeat: 1 validation error")
@requirement("mcpserver:prompt:optional-args")
async def test_get_prompt_with_an_optional_argument_omitted_uses_the_default(connect: Connect) -> None:
"""A prompt rendered without one of its optional arguments uses that parameter's default value."""
mcp = MCPServer("prompter")
@mcp.prompt()
def review(code: str, style: str = "pep8") -> str:
"""Review a snippet of code against a style guide."""
return f"Review {code} per {style}."
async with connect(mcp) as client:
result = await client.get_prompt("review", {"code": "x = 1"})
assert result == snapshot(
GetPromptResult(
description="Review a snippet of code against a style guide.",
messages=[PromptMessage(role="user", content=TextContent(text="Review x = 1 per pep8."))],
)
)
@requirement("mcpserver:prompt:duplicate-name")
async def test_registering_a_duplicate_prompt_name_warns_and_keeps_the_first(connect: Connect) -> None:
"""Registering a second prompt with an already-used name keeps the first registration.
The intended behaviour is rejection at registration time; MCPServer instead logs a warning
and discards the second registration (see the divergence note on the requirement). The
second function is registered via the decorator with an explicit name so the test does not
redefine the same function name in this scope.
"""
mcp = MCPServer("prompter")
@mcp.prompt()
def greet() -> str:
"""The first registration; this is the one that wins."""
return "first"
@mcp.prompt(name="greet")
def greet_second() -> str:
"""Registered with a duplicate name; the registration is discarded so this never runs."""
raise NotImplementedError
async with connect(mcp) as client:
listed = await client.list_prompts()
result = await client.get_prompt("greet")
assert [prompt.name for prompt in listed.prompts] == ["greet"]
assert result == snapshot(
GetPromptResult(
description="The first registration; this is the one that wins.",
messages=[PromptMessage(role="user", content=TextContent(text="first"))],
)
)