Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/servers/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ That is the entire life of a prompt: listed by name, rendered on demand, dropped

!!! check
`required` is enforced before your function runs. Render `review_code` without `code` and the
request itself fails with a JSON-RPC error (code `-32603`):
request itself fails with a JSON-RPC error (code `-32602`, Invalid params):

```text
mcp.shared.exceptions.MCPError: Internal server error
mcp.shared.exceptions.MCPError: Missing required arguments: {'code'}
```

There is no tool-style error result to hand back to a model, because no model is in the loop:
the call raises. The reason (`Missing required arguments: {'code'}`) lands in your server's log.
the call raises. You can distinguish missing required arguments from internal errors by the error code.

### Try it

Expand Down
8 changes: 5 additions & 3 deletions src/mcp/server/mcpserver/prompts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import anyio.to_thread
import pydantic_core
from mcp_types import ContentBlock, Icon, InputRequiredResult, TextContent
from mcp_types import INVALID_PARAMS, ContentBlock, Icon, InputRequiredResult, TextContent
from pydantic import BaseModel, Field, TypeAdapter, validate_call

from mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context
Expand Down Expand Up @@ -166,15 +166,17 @@ async def render(
through unchanged so the multi-round-trip flow reaches the client.
Raises:
ValueError: If required arguments are missing, or if rendering fails.
MCPError: If a required argument is missing, with code `INVALID_PARAMS`,
or if the prompt function raises it.
ValueError: If rendering fails.
"""
# Validate required arguments
if self.arguments:
required = {arg.name for arg in self.arguments if arg.required}
provided = set(arguments or {})
missing = required - provided
if missing:
raise ValueError(f"Missing required arguments: {missing}")
raise MCPError(code=INVALID_PARAMS, message=f"Missing required arguments: {missing}")

try:
# Add context to arguments if needed
Expand Down
13 changes: 10 additions & 3 deletions src/mcp/server/mcpserver/prompts/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

from typing import TYPE_CHECKING, Any

from mcp_types import InputRequiredResult
from mcp_types import INVALID_PARAMS, InputRequiredResult

from mcp.server.mcpserver.prompts.base import Message, Prompt
from mcp.server.mcpserver.utilities.logging import get_logger
from mcp.shared.exceptions import MCPError

if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT
Expand Down Expand Up @@ -59,9 +60,15 @@ async def render_prompt(
arguments: dict[str, Any] | None,
context: Context[LifespanContextT, RequestT],
) -> list[Message] | InputRequiredResult:
"""Render a prompt by name with arguments."""
"""Render a prompt by name with arguments.
Raises:
MCPError: If the prompt name is unknown or a required argument is missing,
with code `INVALID_PARAMS`, or if the prompt function raises it.
ValueError: If rendering fails.
"""
prompt = self.get_prompt(name)
if not prompt:
raise ValueError(f"Unknown prompt: {name}")
raise MCPError(code=INVALID_PARAMS, message=f"Unknown prompt: {name}")

return await prompt.render(arguments, context)
7 changes: 6 additions & 1 deletion src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,13 +1348,18 @@ async def get_prompt(
through unchanged (the 2026-07-28 multi-round-trip flow); the retry's
answers arrive on `ctx.input_responses`, with `ctx.request_state`
carrying the echoed opaque state.
Raises:
MCPError: If the prompt name is unknown or a required argument is missing,
with code `INVALID_PARAMS`, or if the prompt function raises it.
Comment thread
Kludex marked this conversation as resolved.
ValueError: If rendering fails.
"""
if context is None:
context = Context(mcp_server=self, subscriptions=self._subscriptions)
try:
prompt = self._prompt_manager.get_prompt(name)
if not prompt:
raise ValueError(f"Unknown prompt: {name}")
raise MCPError(code=INVALID_PARAMS, message=f"Unknown prompt: {name}")
Comment thread
Kludex marked this conversation as resolved.

rendered = await prompt.render(arguments, context)
if isinstance(rendered, InputRequiredResult):
Expand Down
8 changes: 4 additions & 4 deletions tests/docs_src/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest
from inline_snapshot import snapshot
from mcp_types import PromptArgument, PromptMessage, TextContent
from mcp_types import INVALID_PARAMS, PromptArgument, PromptMessage, TextContent

from docs_src.prompts import tutorial001, tutorial002, tutorial003
from mcp import Client, MCPError
Expand Down Expand Up @@ -54,11 +54,11 @@ async def test_missing_required_argument_is_a_protocol_error() -> None:
async with Client(tutorial001.mcp) as client:
with pytest.raises(MCPError) as exc_info:
await client.get_prompt("review_code")
assert exc_info.value.code == -32603
assert exc_info.value.message == "Internal server error"
assert exc_info.value.code == INVALID_PARAMS
assert exc_info.value.message == snapshot("Missing required arguments: {'code'}")
# The line a traceback prints, exactly as the page quotes it: the code is not in the message.
assert traceback.format_exception_only(exc_info.value) == snapshot(
["mcp.shared.exceptions.MCPError: Internal server error\n"]
["mcp.shared.exceptions.MCPError: Missing required arguments: {'code'}\n"]
)


Expand Down
14 changes: 0 additions & 14 deletions tests/interaction/_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,13 +1372,6 @@ def __post_init__(self) -> None:
"prompts:get:missing-required-args": Requirement(
source=f"{SPEC_BASE_URL}/server/prompts#error-handling",
behavior="prompts/get omitting a required argument returns JSON-RPC error -32602 (Invalid params).",
divergence=Divergence(
note=(
"MCPServer's prompt renderer raises a plain ValueError before the prompt function runs, "
"which the low-level server converts to error code 0 with the exception text as the message."
),
),
arm_exclusions=(ArmExclusion(reason="modern-error-surface", spec_version="2026-07-28"),),
),
"prompts:get:multi-message": Requirement(
source=f"{SPEC_BASE_URL}/server/prompts#getting-a-prompt",
Expand Down Expand Up @@ -1447,13 +1440,6 @@ def __post_init__(self) -> None:
"mcpserver:prompt:unknown-name": Requirement(
source=f"{SPEC_BASE_URL}/server/prompts#error-handling",
behavior="prompts/get for a name that was never registered returns JSON-RPC error -32602 (Invalid params).",
divergence=Divergence(
note=(
"The spec's example uses -32602 Invalid params for unknown prompts; MCPServer raises "
"ValueError, which the low-level server converts to error code 0."
),
),
arm_exclusions=(ArmExclusion(reason="modern-error-surface", spec_version="2026-07-28"),),
),
# ═══════════════════════════════════════════════════════════════════════════
# Completion
Expand Down
14 changes: 7 additions & 7 deletions tests/interaction/mcpserver/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest
from inline_snapshot import snapshot
from mcp_types import (
INVALID_PARAMS,
ErrorData,
GetPromptResult,
ListPromptsResult,
Expand Down Expand Up @@ -78,8 +79,7 @@ def greet(name: str) -> str:
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).
The spec recommends `INVALID_PARAMS` for unknown prompt names.
"""
mcp = MCPServer("prompter")

Expand All @@ -92,16 +92,15 @@ def greet(name: str) -> str:
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"))
assert exc_info.value.code == INVALID_PARAMS
assert exc_info.value.error == snapshot(ErrorData(code=-32602, 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).
The spec recommends `INVALID_PARAMS`; validation rejects the call before the function runs.
"""
mcp = MCPServer("prompter")

Expand All @@ -114,7 +113,8 @@ def greet(name: str) -> str:
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'}"))
assert exc_info.value.code == INVALID_PARAMS
assert exc_info.value.error == snapshot(ErrorData(code=-32602, message="Missing required arguments: {'name'}"))


@requirement("mcpserver:prompt:args-validation")
Expand Down
5 changes: 4 additions & 1 deletion tests/server/mcpserver/prompts/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest
from mcp_types import (
INVALID_PARAMS,
AudioContent,
ElicitRequest,
ElicitRequestFormParams,
Expand All @@ -14,6 +15,7 @@
TextResourceContents,
)

from mcp import MCPError
from mcp.server.mcpserver import AssistantMessage, Audio, Context, Image, MCPServer, Message, UserMessage
from mcp.server.mcpserver.prompts.base import Prompt

Expand Down Expand Up @@ -55,8 +57,9 @@ async def fn(name: str, age: int = 30) -> str: # pragma: no cover
return f"Hello, {name}! You're {age} years old."

prompt = Prompt.from_function(fn)
with pytest.raises(ValueError):
with pytest.raises(MCPError) as exc_info:
await prompt.render({"age": 40}, Context())
assert exc_info.value.code == INVALID_PARAMS

@pytest.mark.anyio
async def test_fn_returns_message(self):
Expand Down
12 changes: 9 additions & 3 deletions tests/server/mcpserver/prompts/test_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from mcp_types import TextContent
from inline_snapshot import snapshot
from mcp_types import INVALID_PARAMS, TextContent

from mcp import MCPError
from mcp.server.mcpserver import Context
from mcp.server.mcpserver.prompts.base import Prompt, UserMessage
from mcp.server.mcpserver.prompts.manager import PromptManager
Expand Down Expand Up @@ -93,8 +95,10 @@ def fn(name: str) -> str:
async def test_render_unknown_prompt(self):
"""Test rendering a non-existent prompt."""
manager = PromptManager()
with pytest.raises(ValueError, match="Unknown prompt: unknown"):
with pytest.raises(MCPError) as exc_info:
await manager.render_prompt("unknown", None, Context())
assert exc_info.value.code == INVALID_PARAMS
assert exc_info.value.message == snapshot("Unknown prompt: unknown")

@pytest.mark.anyio
async def test_render_prompt_with_missing_args(self):
Expand All @@ -106,5 +110,7 @@ def fn(name: str) -> str: # pragma: no cover
manager = PromptManager()
prompt = Prompt.from_function(fn)
manager.add_prompt(prompt)
with pytest.raises(ValueError, match="Missing required arguments"):
with pytest.raises(MCPError) as exc_info:
await manager.render_prompt("fn", None, Context())
assert exc_info.value.code == INVALID_PARAMS
Comment thread
Kludex marked this conversation as resolved.
assert exc_info.value.message == snapshot("Missing required arguments: {'name'}")
8 changes: 6 additions & 2 deletions tests/server/mcpserver/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1565,8 +1565,10 @@ async def test_get_unknown_prompt(self):
mcp = MCPServer()

async with Client(mcp, mode="legacy") as client:
with pytest.raises(MCPError, match="Unknown prompt"):
with pytest.raises(MCPError) as exc_info:
await client.get_prompt("unknown")
assert exc_info.value.code == INVALID_PARAMS
assert exc_info.value.message == snapshot("Unknown prompt: unknown")

async def test_get_prompt_missing_args(self):
"""Test error when required arguments are missing."""
Expand All @@ -1576,8 +1578,10 @@ async def test_get_prompt_missing_args(self):
def prompt_fn(name: str) -> str: ... # pragma: no branch

async with Client(mcp, mode="legacy") as client:
with pytest.raises(MCPError, match="Missing required arguments"):
with pytest.raises(MCPError) as exc_info:
await client.get_prompt("prompt_fn")
assert exc_info.value.code == INVALID_PARAMS
assert exc_info.value.message == snapshot("Missing required arguments: {'name'}")


async def test_resource_decorator_rfc6570_reserved_expansion():
Expand Down
Loading