Skip to content
Closed
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
10 changes: 10 additions & 0 deletions src/mcp/server/mcpserver/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ class ToolError(MCPServerError):

class InvalidSignature(Exception):
"""Invalid signature for use with MCPServer."""


class PromptArgumentError(ValueError):
"""Raised when required prompt arguments are missing or invalid.

Inherits from `ValueError` for backward compatibility (existing code that
catches `ValueError` continues to work), but lets the server log these
known validation errors at warning level instead of exception level with
a full traceback — see #3342.
"""
7 changes: 6 additions & 1 deletion src/mcp/server/mcpserver/prompts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from mcp_types import ContentBlock, Icon, InputRequiredResult, TextContent
from pydantic import BaseModel, Field, TypeAdapter, validate_call

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

try:
# Add context to arguments if needed
Expand Down
12 changes: 11 additions & 1 deletion src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@
from mcp.server.lowlevel.server import LifespanResultT, Server
from mcp.server.lowlevel.server import lifespan as default_lifespan
from mcp.server.mcpserver.context import Context
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError
from mcp.server.mcpserver.exceptions import (
PromptArgumentError,
ResourceError,
ResourceNotFoundError,
)
from mcp.server.mcpserver.prompts import Prompt, PromptManager
from mcp.server.mcpserver.resources import (
DEFAULT_RESOURCE_SECURITY,
Expand Down Expand Up @@ -1298,6 +1302,12 @@ async def get_prompt(
)
except MCPError:
raise
except PromptArgumentError as e:
# Known validation error — log at warning without traceback so
# user-facing "missing required arguments" calls don't pollute
# server logs with full stack traces. See issue #3342.
logger.warning(f"Prompt {name!r} rejected: {e}")
raise ValueError(str(e)) from e
except Exception as e:
logger.exception(f"Error getting prompt {name}")
raise ValueError(str(e)) from e
Expand Down
53 changes: 52 additions & 1 deletion tests/server/mcpserver/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from mcp.client import Client
from mcp.server.context import ServerRequestContext
from mcp.server.mcpserver import Context, MCPServer, ResourceSecurity
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
from mcp.server.mcpserver.exceptions import PromptArgumentError, ResourceNotFoundError, ToolError
from mcp.server.mcpserver.prompts.base import Message, UserMessage
from mcp.server.mcpserver.resources import FileResource, FunctionResource
from mcp.server.mcpserver.utilities.types import Audio, Image
Expand Down Expand Up @@ -1527,6 +1527,57 @@ def prompt_fn(name: str) -> str: ... # pragma: no branch
with pytest.raises(MCPError, match="Missing required arguments"):
await client.get_prompt("prompt_fn")

async def test_get_prompt_missing_args_logs_warning_no_traceback(self, caplog: pytest.LogCaptureFixture):
"""Issue #3342: missing required arguments should log at WARNING level,
not as an exception with a full traceback. The error must still
propagate to the client.
"""
import logging

from mcp.server.mcpserver.utilities.logging import get_logger

mcp = MCPServer()

@mcp.prompt()
def prompt_fn(name: str) -> str: ... # pragma: no branch

logger = get_logger("mcp.server.mcpserver.server")
with caplog.at_level(logging.DEBUG, logger=logger.name):
async with Client(mcp, mode="legacy") as client:
with pytest.raises(MCPError, match="Missing required arguments"):
await client.get_prompt("prompt_fn")

# Should contain a WARNING entry about the rejected prompt.
warning_records = [r for r in caplog.records if r.name == logger.name and r.levelno == logging.WARNING]
assert any("prompt_fn" in r.getMessage() for r in warning_records), (
f"Expected WARNING log about prompt_fn rejection; got {[r.getMessage() for r in caplog.records]}"
)

# Should NOT contain an ERROR/EXCEPTION entry for this rejection —
# those would imply a full traceback.
error_records = [r for r in caplog.records if r.name == logger.name and r.levelno >= logging.ERROR]
assert not any("prompt_fn" in r.getMessage() for r in error_records), (
"Did not expect ERROR/EXCEPTION log for known validation error; "
f"got {[r.getMessage() for r in error_records]}"
)

async def test_prompt_argument_error_is_value_error_subclass(self):
"""Backward compatibility: PromptArgumentError must remain catchable
as ValueError so existing user code keeps working.
"""
mcp = MCPServer()

@mcp.prompt()
def prompt_fn(name: str) -> str: ... # pragma: no branch

prompt_obj = mcp._prompt_manager.get_prompt("prompt_fn")
assert prompt_obj is not None
with pytest.raises(ValueError, match="Missing required arguments"):
await prompt_obj.render(None, context=None) # type: ignore[arg-type]
# And also catchable as the specific subclass.
with pytest.raises(PromptArgumentError, match="Missing required arguments"):
await prompt_obj.render(None, context=None) # type: ignore[arg-type]


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