Skip to content

Commit 5671c8c

Browse files
committed
Return invalid params for unknown prompts and missing arguments
1 parent c449255 commit 5671c8c

9 files changed

Lines changed: 36 additions & 37 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 invalid 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/server.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1348,13 +1348,17 @@ 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.
13511355
"""
13521356
if context is None:
13531357
context = Context(mcp_server=self, subscriptions=self._subscriptions)
13541358
try:
13551359
prompt = self._prompt_manager.get_prompt(name)
13561360
if not prompt:
1357-
raise ValueError(f"Unknown prompt: {name}")
1361+
raise MCPError(code=INVALID_PARAMS, message=f"Unknown prompt: {name}")
13581362

13591363
rendered = await prompt.render(arguments, context)
13601364
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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
2-
from mcp_types import TextContent
2+
from mcp_types import INVALID_PARAMS, TextContent
33

4+
from mcp import MCPError
45
from mcp.server.mcpserver import Context
56
from mcp.server.mcpserver.prompts.base import Prompt, UserMessage
67
from mcp.server.mcpserver.prompts.manager import PromptManager
@@ -106,5 +107,6 @@ def fn(name: str) -> str: # pragma: no cover
106107
manager = PromptManager()
107108
prompt = Prompt.from_function(fn)
108109
manager.add_prompt(prompt)
109-
with pytest.raises(ValueError, match="Missing required arguments"):
110+
with pytest.raises(MCPError) as exc_info:
110111
await manager.render_prompt("fn", None, Context())
112+
assert exc_info.value.code == INVALID_PARAMS

‎tests/server/mcpserver/test_server.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,8 +1565,9 @@ 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
15701571

15711572
async def test_get_prompt_missing_args(self):
15721573
"""Test error when required arguments are missing."""
@@ -1576,8 +1577,9 @@ async def test_get_prompt_missing_args(self):
15761577
def prompt_fn(name: str) -> str: ... # pragma: no branch
15771578

15781579
async with Client(mcp, mode="legacy") as client:
1579-
with pytest.raises(MCPError, match="Missing required arguments"):
1580+
with pytest.raises(MCPError) as exc_info:
15801581
await client.get_prompt("prompt_fn")
1582+
assert exc_info.value.code == INVALID_PARAMS
15811583

15821584

15831585
async def test_resource_decorator_rfc6570_reserved_expansion():

0 commit comments

Comments
 (0)