Skip to content

Commit a6253dd

Browse files
committed
fix(server): log prompt validation errors at WARNING without traceback (#3342)
When a prompt is invoked without a required argument, the request is correctly rejected — but the server logs the resulting ValueError as a full traceback at ERROR level via logger.exception(). For expected, user-facing validation failures (missing required arguments) this floods server logs with stack traces that operators have to wade through. Changes: * Add PromptArgumentError(ValueError) to mcp.server.mcpserver.exceptions — stays a ValueError subclass for backward compatibility, but lets the server distinguish known user-input failures from unexpected bugs. * In Prompt.render(), raise PromptArgumentError instead of bare ValueError. * In MCPServer.get_prompt(), add an except clause that logs PromptArgumentError at WARNING without exc_info, then re-raises the same ValueError the protocol layer expects. * Tests: - test_get_prompt_missing_args_logs_warning_no_traceback — verifies WARNING log is emitted and no ERROR/EXCEPTION log is produced for known validation errors. - test_prompt_argument_error_is_value_error_subclass — verifies backward compat: existing user code catching ValueError still catches the new exception. 22/22 prompt tests pass locally (21 pre-existing + 1 pre-existing test plus my 2 new tests). All 21 existing prompt tests still pass.
1 parent 57394b0 commit a6253dd

4 files changed

Lines changed: 79 additions & 3 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,13 @@ class ToolError(MCPServerError):
2424

2525
class InvalidSignature(Exception):
2626
"""Invalid signature for use with MCPServer."""
27+
28+
29+
class PromptArgumentError(ValueError):
30+
"""Raised when required prompt arguments are missing or invalid.
31+
32+
Inherits from `ValueError` for backward compatibility (existing code that
33+
catches `ValueError` continues to work), but lets the server log these
34+
known validation errors at warning level instead of exception level with
35+
a full traceback — see #3342.
36+
"""

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from mcp_types import ContentBlock, Icon, InputRequiredResult, TextContent
1212
from pydantic import BaseModel, Field, TypeAdapter, validate_call
1313

14+
from mcp.server.mcpserver.exceptions import PromptArgumentError
1415
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context
1516
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
1617
from mcp.server.mcpserver.utilities.types import Audio, Image
@@ -174,7 +175,11 @@ async def render(
174175
provided = set(arguments or {})
175176
missing = required - provided
176177
if missing:
177-
raise ValueError(f"Missing required arguments: {missing}")
178+
# PromptArgumentError is a ValueError subclass for backward
179+
# compatibility — it just lets the server log at warning level
180+
# instead of exception-with-traceback for known validation
181+
# failures (see issue #3342).
182+
raise PromptArgumentError(f"Missing required arguments: {missing}")
178183

179184
try:
180185
# Add context to arguments if needed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@
7171
from mcp.server.lowlevel.server import LifespanResultT, Server
7272
from mcp.server.lowlevel.server import lifespan as default_lifespan
7373
from mcp.server.mcpserver.context import Context
74-
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError
74+
from mcp.server.mcpserver.exceptions import (
75+
PromptArgumentError,
76+
ResourceError,
77+
ResourceNotFoundError,
78+
)
7579
from mcp.server.mcpserver.prompts import Prompt, PromptManager
7680
from mcp.server.mcpserver.resources import (
7781
DEFAULT_RESOURCE_SECURITY,
@@ -1298,6 +1302,12 @@ async def get_prompt(
12981302
)
12991303
except MCPError:
13001304
raise
1305+
except PromptArgumentError as e:
1306+
# Known validation error — log at warning without traceback so
1307+
# user-facing "missing required arguments" calls don't pollute
1308+
# server logs with full stack traces. See issue #3342.
1309+
logger.warning(f"Prompt {name!r} rejected: {e}")
1310+
raise ValueError(str(e)) from e
13011311
except Exception as e:
13021312
logger.exception(f"Error getting prompt {name}")
13031313
raise ValueError(str(e)) from e

‎tests/server/mcpserver/test_server.py‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from mcp.client import Client
5151
from mcp.server.context import ServerRequestContext
5252
from mcp.server.mcpserver import Context, MCPServer, ResourceSecurity
53-
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
53+
from mcp.server.mcpserver.exceptions import PromptArgumentError, ResourceNotFoundError, ToolError
5454
from mcp.server.mcpserver.prompts.base import Message, UserMessage
5555
from mcp.server.mcpserver.resources import FileResource, FunctionResource
5656
from mcp.server.mcpserver.utilities.types import Audio, Image
@@ -1527,6 +1527,57 @@ def prompt_fn(name: str) -> str: ... # pragma: no branch
15271527
with pytest.raises(MCPError, match="Missing required arguments"):
15281528
await client.get_prompt("prompt_fn")
15291529

1530+
async def test_get_prompt_missing_args_logs_warning_no_traceback(self, caplog: pytest.LogCaptureFixture):
1531+
"""Issue #3342: missing required arguments should log at WARNING level,
1532+
not as an exception with a full traceback. The error must still
1533+
propagate to the client.
1534+
"""
1535+
import logging
1536+
1537+
from mcp.server.mcpserver.utilities.logging import get_logger
1538+
1539+
mcp = MCPServer()
1540+
1541+
@mcp.prompt()
1542+
def prompt_fn(name: str) -> str: ... # pragma: no branch
1543+
1544+
logger = get_logger("mcp.server.mcpserver.server")
1545+
with caplog.at_level(logging.DEBUG, logger=logger.name):
1546+
async with Client(mcp, mode="legacy") as client:
1547+
with pytest.raises(MCPError, match="Missing required arguments"):
1548+
await client.get_prompt("prompt_fn")
1549+
1550+
# Should contain a WARNING entry about the rejected prompt.
1551+
warning_records = [r for r in caplog.records if r.name == logger.name and r.levelno == logging.WARNING]
1552+
assert any("prompt_fn" in r.getMessage() for r in warning_records), (
1553+
f"Expected WARNING log about prompt_fn rejection; got {[r.getMessage() for r in caplog.records]}"
1554+
)
1555+
1556+
# Should NOT contain an ERROR/EXCEPTION entry for this rejection —
1557+
# those would imply a full traceback.
1558+
error_records = [r for r in caplog.records if r.name == logger.name and r.levelno >= logging.ERROR]
1559+
assert not any("prompt_fn" in r.getMessage() for r in error_records), (
1560+
"Did not expect ERROR/EXCEPTION log for known validation error; "
1561+
f"got {[r.getMessage() for r in error_records]}"
1562+
)
1563+
1564+
async def test_prompt_argument_error_is_value_error_subclass(self):
1565+
"""Backward compatibility: PromptArgumentError must remain catchable
1566+
as ValueError so existing user code keeps working.
1567+
"""
1568+
mcp = MCPServer()
1569+
1570+
@mcp.prompt()
1571+
def prompt_fn(name: str) -> str: ... # pragma: no branch
1572+
1573+
prompt_obj = mcp._prompt_manager.get_prompt("prompt_fn")
1574+
assert prompt_obj is not None
1575+
with pytest.raises(ValueError, match="Missing required arguments"):
1576+
await prompt_obj.render(None, context=None) # type: ignore[arg-type]
1577+
# And also catchable as the specific subclass.
1578+
with pytest.raises(PromptArgumentError, match="Missing required arguments"):
1579+
await prompt_obj.render(None, context=None) # type: ignore[arg-type]
1580+
15301581

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

0 commit comments

Comments
 (0)