Skip to content

Commit 809dafb

Browse files
committed
Created PromptValidationError for user-input validation failures.
1 parent b83a222 commit 809dafb

4 files changed

Lines changed: 49 additions & 9 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .base import Prompt
1+
from .base import Prompt, PromptValidationError
22
from .manager import PromptManager
33

4-
__all__ = ["Prompt", "PromptManager"]
4+
__all__ = ["Prompt", "PromptManager", "PromptValidationError"]

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@
2222
from mcp.server.mcpserver.context import Context
2323

2424

25+
class PromptValidationError(ValueError):
26+
"""Raised when prompt arguments fail validation (e.g. a required argument is missing).
27+
28+
A `ValueError` subclass so existing callers that catch `ValueError` are
29+
unaffected. It exists to distinguish this expected, user-input validation
30+
failure from the generic `ValueError` `Prompt.render` also raises when the
31+
prompt function itself raises an unexpected exception - callers that log
32+
the two differently (see `MCPServer.get_prompt`) can `except` this
33+
specifically without downgrading a genuine crash to a quiet warning.
34+
"""
35+
36+
2537
class Message(BaseModel):
2638
"""Base class for all prompt messages.
2739
@@ -166,15 +178,15 @@ async def render(
166178
through unchanged so the multi-round-trip flow reaches the client.
167179
168180
Raises:
169-
ValueError: If required arguments are missing, or if rendering fails.
181+
PromptValidationError: If required arguments are missing, or if rendering fails.
170182
"""
171183
# Validate required arguments
172184
if self.arguments:
173185
required = {arg.name for arg in self.arguments if arg.required}
174186
provided = set(arguments or {})
175187
missing = required - provided
176188
if missing:
177-
raise ValueError(f"Missing required arguments: {missing}")
189+
raise PromptValidationError(f"Missing required arguments: {missing}")
178190

179191
try:
180192
# Add context to arguments if needed

src/mcp/server/mcpserver/server.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
from mcp.server.lowlevel.server import lifespan as default_lifespan
7373
from mcp.server.mcpserver.context import Context
7474
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError
75-
from mcp.server.mcpserver.prompts import Prompt, PromptManager
75+
from mcp.server.mcpserver.prompts import Prompt, PromptManager, PromptValidationError
7676
from mcp.server.mcpserver.resources import (
7777
DEFAULT_RESOURCE_SECURITY,
7878
FunctionResource,
@@ -1301,10 +1301,15 @@ async def get_prompt(
13011301
except MCPError:
13021302
raise
13031303

1304-
except ValueError as e:
1304+
except PromptValidationError as e:
13051305
# Expected user-input validation failures, like missing required
1306-
# arguments, don't need a full traceback for unexpected errors.
1307-
# Instead, log a concise warning without exc_info.
1306+
# arguments, don't need a full traceback. Log a concise warning
1307+
# instead of the exc_info dump reserved for unexpected errors.
1308+
1309+
# `Prompt.render` also raises a plain `ValueError` when the prompt
1310+
# function itself throws, so this narrower type is caught here
1311+
# instead of `ValueError` to avoid swallowing that traceback too.
1312+
13081313
logger.warning(f"Error getting prompt {name}: {e}")
13091314
raise ValueError(str(e)) from e
13101315

tests/server/mcpserver/test_server.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1543,7 +1543,7 @@ async def test_get_prompt_missing_args_logs_warning_without_traceback(
15431543
mcp = MCPServer()
15441544

15451545
@mcp.prompt()
1546-
def prompt_fn(name: str) -> str: ... # Pragma: no branch.
1546+
def prompt_fn(name: str) -> str: ... # pragma: no branch.
15471547

15481548
with caplog.at_level(logging.WARNING, logger="mcp.server.mcpserver.server"):
15491549
async with Client(mcp, mode="legacy") as client:
@@ -1557,6 +1557,29 @@ def prompt_fn(name: str) -> str: ... # Pragma: no branch.
15571557
assert server_records[0].levelno == logging.WARNING
15581558
assert not server_records[0].exc_info
15591559

1560+
async def test_get_prompt_unexpected_error_still_logs_traceback(self, caplog: pytest.LogCaptureFixture) -> None:
1561+
"""A prompt function raising an unexpected (non-validation) exception must
1562+
still be logged with a full traceback, even though `Prompt.render` also
1563+
wraps it as a plain `ValueError` — same wire type as the missing-argument
1564+
case, but not a `PromptValidationError`, so it must not be downgraded."""
1565+
mcp = MCPServer()
1566+
1567+
@mcp.prompt()
1568+
def prompt_fn() -> str:
1569+
raise KeyError("boom")
1570+
1571+
with caplog.at_level(logging.WARNING, logger="mcp.server.mcpserver.server"):
1572+
async with Client(mcp, mode="legacy") as client:
1573+
with pytest.raises(MCPError):
1574+
await client.get_prompt("prompt_fn")
1575+
1576+
server_records = [r for r in caplog.records if r.name == "mcp.server.mcpserver.server"]
1577+
assert len(server_records) == 1
1578+
1579+
# Unexpected errors should have error log with exc_info.
1580+
assert server_records[0].levelno == logging.ERROR
1581+
assert server_records[0].exc_info is not None
1582+
15601583

15611584
async def test_resource_decorator_rfc6570_reserved_expansion():
15621585
# Regression: old regex-based param extraction couldn't see `path`

0 commit comments

Comments
 (0)