Skip to content

Commit 9f32b36

Browse files
Support meta on prompts, as tools and resources already do
The 2026-07-28 specification puts an optional `_meta` on the Prompt schema, and `mcp_types.Prompt` already carries it. Only the high-level layer was missing: `@mcp.prompt(meta=...)` raised TypeError, `Prompt` had no `meta` field, and `MCPServer.list_prompts` never populated `_meta`. Wires it through the same way `@mcp.tool` and `@mcp.resource` already do: a `meta` field on the high-level `Prompt`, a `meta` argument on `Prompt.from_function` and on the decorator, and `_meta=prompt.meta` in `list_prompts`. Left alone deliberately: `get_prompt` does not carry meta into the protocol response. That mirrors `read_resource`, which does not either, and the issue asks only for `list_prompts`. Closes #3476 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxphSQWkGcC4sgncbRU64C
1 parent 9972c21 commit 9f32b36

3 files changed

Lines changed: 63 additions & 1 deletion

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class Prompt(BaseModel):
9292
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
9393
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
9494
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")
95+
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this prompt")
9596
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True)
9697

9798
@classmethod
@@ -102,6 +103,7 @@ def from_function(
102103
title: str | None = None,
103104
description: str | None = None,
104105
icons: list[Icon] | None = None,
106+
meta: dict[str, Any] | None = None,
105107
context_kwarg: str | None = None,
106108
) -> Prompt:
107109
"""Create a Prompt from a function.
@@ -152,6 +154,7 @@ def from_function(
152154
arguments=arguments,
153155
fn=fn,
154156
icons=icons,
157+
meta=meta,
155158
context_kwarg=context_kwarg,
156159
)
157160

src/mcp/server/mcpserver/server.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,7 @@ def prompt(
962962
title: str | None = None,
963963
description: str | None = None,
964964
icons: list[Icon] | None = None,
965+
meta: dict[str, Any] | None = None,
965966
) -> Callable[[_CallableT], _CallableT]:
966967
"""Decorator to register a prompt.
967968
@@ -975,6 +976,7 @@ def prompt(
975976
title: Optional human-readable title for the prompt
976977
description: Optional description of what the prompt does
977978
icons: Optional list of icons for the prompt
979+
meta: Optional metadata dictionary for the prompt
978980
979981
Example:
980982
```python
@@ -1013,7 +1015,7 @@ async def analyze_file(path: str) -> list[Message]:
10131015
)
10141016

10151017
def decorator(func: _CallableT) -> _CallableT:
1016-
prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons)
1018+
prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons, meta=meta)
10171019
self.add_prompt(prompt)
10181020
return func
10191021

@@ -1326,6 +1328,7 @@ async def list_prompts(self) -> list[MCPPrompt]:
13261328
for arg in (prompt.arguments or [])
13271329
],
13281330
icons=prompt.icons,
1331+
_meta=prompt.meta,
13291332
)
13301333
for prompt in prompts
13311334
]

tests/server/mcpserver/test_server.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
UnexpectedToolError,
6161
)
6262
from mcp.server.mcpserver.prompts.base import Message, UserMessage
63+
from mcp.server.mcpserver.prompts.base import Prompt as MCPServerPrompt
6364
from mcp.server.mcpserver.resources import FileResource, FunctionResource
6465
from mcp.server.mcpserver.resources import Resource as MCPServerResource
6566
from mcp.server.mcpserver.utilities.types import Audio, Image
@@ -3186,3 +3187,58 @@ async def refuse_listen(ctx: ServerRequestContext[Any, Any], call_next: Any) ->
31863187
pass # pragma: no cover - the refusal precedes the stream
31873188
assert exc_info.value.error.code == INVALID_REQUEST
31883189
assert exc_info.value.error.message == "not permitted to watch the requested resources"
3190+
3191+
3192+
class TestServerPromptMetadata:
3193+
"""Test MCPServer @prompt decorator meta parameter for list operations.
3194+
3195+
Meta flows: @prompt decorator -> Prompt.from_function -> Prompt.meta -> list_prompts.
3196+
"""
3197+
3198+
async def test_prompt_decorator_with_metadata(self):
3199+
"""Test that @prompt decorator accepts and passes meta parameter."""
3200+
mcp = MCPServer()
3201+
3202+
@mcp.prompt(name="code_review", title="Code Review", meta={"strictness": "high"})
3203+
def review_code(code: str) -> str:
3204+
"""Review code with specific metadata rules."""
3205+
return f"Review this: {code}" # pragma: no cover
3206+
3207+
prompts = await mcp.list_prompts()
3208+
assert prompts == snapshot(
3209+
[
3210+
Prompt(
3211+
name="code_review",
3212+
title="Code Review",
3213+
description="Review code with specific metadata rules.",
3214+
arguments=[PromptArgument(name="code", required=True)],
3215+
meta={"strictness": "high"}, # type: ignore[reportCallIssue]
3216+
)
3217+
]
3218+
)
3219+
3220+
async def test_prompt_without_metadata_has_no_meta(self):
3221+
"""A prompt that declares no meta must not emit an empty _meta."""
3222+
mcp = MCPServer()
3223+
3224+
@mcp.prompt()
3225+
def plain(code: str) -> str:
3226+
"""No metadata here."""
3227+
return code # pragma: no cover
3228+
3229+
prompts = await mcp.list_prompts()
3230+
assert prompts[0].meta is None
3231+
assert "_meta" not in prompts[0].model_dump(by_alias=True, exclude_none=True)
3232+
3233+
async def test_add_prompt_preserves_metadata(self):
3234+
"""Meta survives the non-decorator registration path too."""
3235+
3236+
def review_code(code: str) -> str:
3237+
"""Review code."""
3238+
return code # pragma: no cover
3239+
3240+
mcp = MCPServer()
3241+
mcp.add_prompt(MCPServerPrompt.from_function(review_code, meta={"strictness": "high"}))
3242+
3243+
prompts = await mcp.list_prompts()
3244+
assert prompts[0].meta == {"strictness": "high"}

0 commit comments

Comments
 (0)