Skip to content

Commit 754254c

Browse files
authored
Return invalid params for unknown prompts and missing arguments (#3696)
1 parent 9db497e commit 754254c

10 files changed

Lines changed: 54 additions & 41 deletions

File tree

‎docs/servers/prompts.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ That is the entire life of a prompt: listed by name, rendered on demand, dropped
5656

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

6161
```text
62-
mcp.shared.exceptions.MCPError: Internal server error
62+
mcp.shared.exceptions.MCPError: Missing required arguments: {'code'}
6363
```
6464

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

6868
### Try it
6969

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import anyio.to_thread
1010
import pydantic_core
11-
from mcp_types import ContentBlock, Icon, InputRequiredResult, TextContent
11+
from mcp_types import INVALID_PARAMS, ContentBlock, Icon, InputRequiredResult, TextContent
1212
from pydantic import BaseModel, Field, TypeAdapter, validate_call
1313

1414
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context
@@ -166,15 +166,17 @@ async def render(
166166
through unchanged so the multi-round-trip flow reaches the client.
167167
168168
Raises:
169-
ValueError: If required arguments are missing, or if rendering fails.
169+
MCPError: If a required argument is missing, with code `INVALID_PARAMS`,
170+
or if the prompt function raises it.
171+
ValueError: If rendering fails.
170172
"""
171173
# Validate required arguments
172174
if self.arguments:
173175
required = {arg.name for arg in self.arguments if arg.required}
174176
provided = set(arguments or {})
175177
missing = required - provided
176178
if missing:
177-
raise ValueError(f"Missing required arguments: {missing}")
179+
raise MCPError(code=INVALID_PARAMS, message=f"Missing required arguments: {missing}")
178180

179181
try:
180182
# Add context to arguments if needed

‎src/mcp/server/mcpserver/prompts/manager.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
from typing import TYPE_CHECKING, Any
66

7-
from mcp_types import InputRequiredResult
7+
from mcp_types import INVALID_PARAMS, InputRequiredResult
88

99
from mcp.server.mcpserver.prompts.base import Message, Prompt
1010
from mcp.server.mcpserver.utilities.logging import get_logger
11+
from mcp.shared.exceptions import MCPError
1112

1213
if TYPE_CHECKING:
1314
from mcp.server.context import LifespanContextT, RequestT
@@ -59,9 +60,15 @@ async def render_prompt(
5960
arguments: dict[str, Any] | None,
6061
context: Context[LifespanContextT, RequestT],
6162
) -> list[Message] | InputRequiredResult:
62-
"""Render a prompt by name with arguments."""
63+
"""Render a prompt by name with arguments.
64+
65+
Raises:
66+
MCPError: If the prompt name is unknown or a required argument is missing,
67+
with code `INVALID_PARAMS`, or if the prompt function raises it.
68+
ValueError: If rendering fails.
69+
"""
6370
prompt = self.get_prompt(name)
6471
if not prompt:
65-
raise ValueError(f"Unknown prompt: {name}")
72+
raise MCPError(code=INVALID_PARAMS, message=f"Unknown prompt: {name}")
6673

6774
return await prompt.render(arguments, context)

‎src/mcp/server/mcpserver/server.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1348,13 +1348,18 @@ async def get_prompt(
13481348
through unchanged (the 2026-07-28 multi-round-trip flow); the retry's
13491349
answers arrive on `ctx.input_responses`, with `ctx.request_state`
13501350
carrying the echoed opaque state.
1351+
1352+
Raises:
1353+
MCPError: If the prompt name is unknown or a required argument is missing,
1354+
with code `INVALID_PARAMS`, or if the prompt function raises it.
1355+
ValueError: If rendering fails.
13511356
"""
13521357
if context is None:
13531358
context = Context(mcp_server=self, subscriptions=self._subscriptions)
13541359
try:
13551360
prompt = self._prompt_manager.get_prompt(name)
13561361
if not prompt:
1357-
raise ValueError(f"Unknown prompt: {name}")
1362+
raise MCPError(code=INVALID_PARAMS, message=f"Unknown prompt: {name}")
13581363

13591364
rendered = await prompt.render(arguments, context)
13601365
if isinstance(rendered, InputRequiredResult):

‎tests/docs_src/test_prompts.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66
from inline_snapshot import snapshot
7-
from mcp_types import PromptArgument, PromptMessage, TextContent
7+
from mcp_types import INVALID_PARAMS, PromptArgument, PromptMessage, TextContent
88

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

6464

‎tests/interaction/_requirements.py‎

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,13 +1372,6 @@ def __post_init__(self) -> None:
13721372
"prompts:get:missing-required-args": Requirement(
13731373
source=f"{SPEC_BASE_URL}/server/prompts#error-handling",
13741374
behavior="prompts/get omitting a required argument returns JSON-RPC error -32602 (Invalid params).",
1375-
divergence=Divergence(
1376-
note=(
1377-
"MCPServer's prompt renderer raises a plain ValueError before the prompt function runs, "
1378-
"which the low-level server converts to error code 0 with the exception text as the message."
1379-
),
1380-
),
1381-
arm_exclusions=(ArmExclusion(reason="modern-error-surface", spec_version="2026-07-28"),),
13821375
),
13831376
"prompts:get:multi-message": Requirement(
13841377
source=f"{SPEC_BASE_URL}/server/prompts#getting-a-prompt",
@@ -1447,13 +1440,6 @@ def __post_init__(self) -> None:
14471440
"mcpserver:prompt:unknown-name": Requirement(
14481441
source=f"{SPEC_BASE_URL}/server/prompts#error-handling",
14491442
behavior="prompts/get for a name that was never registered returns JSON-RPC error -32602 (Invalid params).",
1450-
divergence=Divergence(
1451-
note=(
1452-
"The spec's example uses -32602 Invalid params for unknown prompts; MCPServer raises "
1453-
"ValueError, which the low-level server converts to error code 0."
1454-
),
1455-
),
1456-
arm_exclusions=(ArmExclusion(reason="modern-error-surface", spec_version="2026-07-28"),),
14571443
),
14581444
# ═══════════════════════════════════════════════════════════════════════════
14591445
# Completion

‎tests/interaction/mcpserver/test_prompts.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from inline_snapshot import snapshot
55
from mcp_types import (
6+
INVALID_PARAMS,
67
ErrorData,
78
GetPromptResult,
89
ListPromptsResult,
@@ -78,8 +79,7 @@ def greet(name: str) -> str:
7879
async def test_get_unknown_prompt_is_error(connect: Connect) -> None:
7980
"""Getting a prompt name that was never registered fails with a JSON-RPC error.
8081
81-
The spec reserves -32602 for this case; the SDK reports code 0 (see the divergence note on
82-
the requirement).
82+
The spec recommends `INVALID_PARAMS` for unknown prompt names.
8383
"""
8484
mcp = MCPServer("prompter")
8585

@@ -92,16 +92,15 @@ def greet(name: str) -> str:
9292
with pytest.raises(MCPError) as exc_info:
9393
await client.get_prompt("nope")
9494

95-
assert exc_info.value.error == snapshot(ErrorData(code=0, message="Unknown prompt: nope"))
95+
assert exc_info.value.code == INVALID_PARAMS
96+
assert exc_info.value.error == snapshot(ErrorData(code=-32602, message="Unknown prompt: nope"))
9697

9798

9899
@requirement("prompts:get:missing-required-args")
99100
async def test_get_prompt_with_a_missing_required_argument_is_an_error(connect: Connect) -> None:
100101
"""Getting a prompt without one of its required arguments fails with a JSON-RPC error.
101102
102-
The missing argument is detected before the prompt function is called, but the spec's -32602
103-
Invalid params is reported as error code 0 with the bare exception text (see the divergence
104-
note on the requirement).
103+
The spec recommends `INVALID_PARAMS`; validation rejects the call before the function runs.
105104
"""
106105
mcp = MCPServer("prompter")
107106

@@ -114,7 +113,8 @@ def greet(name: str) -> str:
114113
with pytest.raises(MCPError) as exc_info:
115114
await client.get_prompt("greet")
116115

117-
assert exc_info.value.error == snapshot(ErrorData(code=0, message="Missing required arguments: {'name'}"))
116+
assert exc_info.value.code == INVALID_PARAMS
117+
assert exc_info.value.error == snapshot(ErrorData(code=-32602, message="Missing required arguments: {'name'}"))
118118

119119

120120
@requirement("mcpserver:prompt:args-validation")

‎tests/server/mcpserver/prompts/test_base.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66
from mcp_types import (
7+
INVALID_PARAMS,
78
AudioContent,
89
ElicitRequest,
910
ElicitRequestFormParams,
@@ -14,6 +15,7 @@
1415
TextResourceContents,
1516
)
1617

18+
from mcp import MCPError
1719
from mcp.server.mcpserver import AssistantMessage, Audio, Context, Image, MCPServer, Message, UserMessage
1820
from mcp.server.mcpserver.prompts.base import Prompt
1921

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

5759
prompt = Prompt.from_function(fn)
58-
with pytest.raises(ValueError):
60+
with pytest.raises(MCPError) as exc_info:
5961
await prompt.render({"age": 40}, Context())
62+
assert exc_info.value.code == INVALID_PARAMS
6063

6164
@pytest.mark.anyio
6265
async def test_fn_returns_message(self):

‎tests/server/mcpserver/prompts/test_manager.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
2-
from mcp_types import TextContent
2+
from inline_snapshot import snapshot
3+
from mcp_types import INVALID_PARAMS, TextContent
34

5+
from mcp import MCPError
46
from mcp.server.mcpserver import Context
57
from mcp.server.mcpserver.prompts.base import Prompt, UserMessage
68
from mcp.server.mcpserver.prompts.manager import PromptManager
@@ -93,8 +95,10 @@ def fn(name: str) -> str:
9395
async def test_render_unknown_prompt(self):
9496
"""Test rendering a non-existent prompt."""
9597
manager = PromptManager()
96-
with pytest.raises(ValueError, match="Unknown prompt: unknown"):
98+
with pytest.raises(MCPError) as exc_info:
9799
await manager.render_prompt("unknown", None, Context())
100+
assert exc_info.value.code == INVALID_PARAMS
101+
assert exc_info.value.message == snapshot("Unknown prompt: unknown")
98102

99103
@pytest.mark.anyio
100104
async def test_render_prompt_with_missing_args(self):
@@ -106,5 +110,7 @@ def fn(name: str) -> str: # pragma: no cover
106110
manager = PromptManager()
107111
prompt = Prompt.from_function(fn)
108112
manager.add_prompt(prompt)
109-
with pytest.raises(ValueError, match="Missing required arguments"):
113+
with pytest.raises(MCPError) as exc_info:
110114
await manager.render_prompt("fn", None, Context())
115+
assert exc_info.value.code == INVALID_PARAMS
116+
assert exc_info.value.message == snapshot("Missing required arguments: {'name'}")

‎tests/server/mcpserver/test_server.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,8 +1565,10 @@ async def test_get_unknown_prompt(self):
15651565
mcp = MCPServer()
15661566

15671567
async with Client(mcp, mode="legacy") as client:
1568-
with pytest.raises(MCPError, match="Unknown prompt"):
1568+
with pytest.raises(MCPError) as exc_info:
15691569
await client.get_prompt("unknown")
1570+
assert exc_info.value.code == INVALID_PARAMS
1571+
assert exc_info.value.message == snapshot("Unknown prompt: unknown")
15701572

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

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

15821586

15831587
async def test_resource_decorator_rfc6570_reserved_expansion():

0 commit comments

Comments
 (0)