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
3 changes: 3 additions & 0 deletions src/mcp/server/mcpserver/prompts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class Prompt(BaseModel):
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this prompt")
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True)

@classmethod
Expand All @@ -102,6 +103,7 @@ def from_function(
title: str | None = None,
description: str | None = None,
icons: list[Icon] | None = None,
meta: dict[str, Any] | None = None,
context_kwarg: str | None = None,
) -> Prompt:
"""Create a Prompt from a function.
Expand Down Expand Up @@ -152,6 +154,7 @@ def from_function(
arguments=arguments,
fn=fn,
icons=icons,
meta=meta,
context_kwarg=context_kwarg,
)

Expand Down
5 changes: 4 additions & 1 deletion src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@ def prompt(
title: str | None = None,
description: str | None = None,
icons: list[Icon] | None = None,
meta: dict[str, Any] | None = None,
) -> Callable[[_CallableT], _CallableT]:
"""Decorator to register a prompt.

Expand All @@ -975,6 +976,7 @@ def prompt(
title: Optional human-readable title for the prompt
description: Optional description of what the prompt does
icons: Optional list of icons for the prompt
meta: Optional metadata dictionary for the prompt

Example:
```python
Expand Down Expand Up @@ -1013,7 +1015,7 @@ async def analyze_file(path: str) -> list[Message]:
)

def decorator(func: _CallableT) -> _CallableT:
prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons)
prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons, meta=meta)
self.add_prompt(prompt)
return func

Expand Down Expand Up @@ -1326,6 +1328,7 @@ async def list_prompts(self) -> list[MCPPrompt]:
for arg in (prompt.arguments or [])
],
icons=prompt.icons,
_meta=prompt.meta,
)
for prompt in prompts
]
Expand Down
56 changes: 56 additions & 0 deletions tests/server/mcpserver/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
UnexpectedToolError,
)
from mcp.server.mcpserver.prompts.base import Message, UserMessage
from mcp.server.mcpserver.prompts.base import Prompt as MCPServerPrompt
from mcp.server.mcpserver.resources import FileResource, FunctionResource
from mcp.server.mcpserver.resources import Resource as MCPServerResource
from mcp.server.mcpserver.utilities.types import Audio, Image
Expand Down Expand Up @@ -3186,3 +3187,58 @@ async def refuse_listen(ctx: ServerRequestContext[Any, Any], call_next: Any) ->
pass # pragma: no cover - the refusal precedes the stream
assert exc_info.value.error.code == INVALID_REQUEST
assert exc_info.value.error.message == "not permitted to watch the requested resources"


class TestServerPromptMetadata:
"""Test MCPServer @prompt decorator meta parameter for list operations.

Meta flows: @prompt decorator -> Prompt.from_function -> Prompt.meta -> list_prompts.
"""

async def test_prompt_decorator_with_metadata(self):
"""Test that @prompt decorator accepts and passes meta parameter."""
mcp = MCPServer()

@mcp.prompt(name="code_review", title="Code Review", meta={"strictness": "high"})
def review_code(code: str) -> str:
"""Review code with specific metadata rules."""
return f"Review this: {code}" # pragma: no cover

prompts = await mcp.list_prompts()
assert prompts == snapshot(
[
Prompt(
name="code_review",
title="Code Review",
description="Review code with specific metadata rules.",
arguments=[PromptArgument(name="code", required=True)],
meta={"strictness": "high"}, # type: ignore[reportCallIssue]
)
]
)

async def test_prompt_without_metadata_has_no_meta(self):
"""A prompt that declares no meta must not emit an empty _meta."""
mcp = MCPServer()

@mcp.prompt()
def plain(code: str) -> str:
"""No metadata here."""
return code # pragma: no cover

prompts = await mcp.list_prompts()
assert prompts[0].meta is None
assert "_meta" not in prompts[0].model_dump(by_alias=True, exclude_none=True)

async def test_add_prompt_preserves_metadata(self):
"""Meta survives the non-decorator registration path too."""

def review_code(code: str) -> str:
"""Review code."""
return code # pragma: no cover

mcp = MCPServer()
mcp.add_prompt(MCPServerPrompt.from_function(review_code, meta={"strictness": "high"}))

prompts = await mcp.list_prompts()
assert prompts[0].meta == {"strictness": "high"}
Loading