Skip to content

Commit d26de07

Browse files
committed
Let MCPServer.add_prompt take a function; export prompt Message classes
add_tool(fn) registers a function but add_prompt() only took a ready-made Prompt, so registering a prompt outside the decorator meant importing Prompt from a subpackage and calling Prompt.from_function yourself. add_prompt() now also accepts the function with the same keyword options as @prompt(); the Prompt form (including add_prompt(prompt=...)) is unchanged and @prompt() still hands add_prompt a Prompt, so subclass overrides keep intercepting registrations. Message, UserMessage and AssistantMessage are re-exported from mcp.server.mcpserver next to Image and Audio.
1 parent 0be83f5 commit d26de07

4 files changed

Lines changed: 84 additions & 4 deletions

File tree

docs/migration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ All submodules under `mcp.server.fastmcp.*` are now under `mcp.server.mcpserver.
668668

669669
- `Image`, `Audio` — from `mcp.server.mcpserver` (or `.utilities.types`)
670670
- `Icon` — from `mcp.server.mcpserver` or `mcp.types` (not a top-level `mcp` export); its `mimeType` field is now `mime_type` per the [snake_case renames](#field-names-changed-from-camelcase-to-snake_case), though the `mimeType=` kwarg still constructs
671-
- `Message`, `UserMessage`, `AssistantMessage` — from `mcp.server.mcpserver.prompts.base`
671+
- `Message`, `UserMessage`, `AssistantMessage` — from `mcp.server.mcpserver` (or `.prompts.base`)
672672
- `ToolError`, `ResourceError` — from `mcp.server.mcpserver.exceptions`
673673
- `MCPServerError` (renamed from `FastMCPError`) — from `mcp.server.mcpserver.exceptions`
674674

@@ -678,7 +678,7 @@ Beyond the changes covered in this section, the everyday `FastMCP` surface carri
678678

679679
- **Decorators.** `@mcp.tool()`, `@mcp.resource()`, `@mcp.prompt()`, and `@mcp.completion()` take the same arguments and handler signatures as v1. The lowlevel [`on_completion` reshape](#lowlevel-server-decorator-based-handlers-replaced-with-constructor-on_-params) applies only to the lowlevel `Server`; a high-level `@mcp.completion()` handler is still called as `(ref, argument, context)`.
680680
- **Tool return handling.** A returned `CallToolResult` (including an `Annotated[CallToolResult, YourModel]` output schema, and `_meta`) is passed through, `Image` and `Audio` convert to content blocks as before, ready-made content blocks are kept as-is (neither is [structured by default](#content-block-image-and-audio-return-annotations-are-unstructured) now, even inside a `list`), and dict, list, scalar, and model returns are wrapped into `content` and `structured_content` by the same rules.
681-
- **Listing and registration methods.** `list_tools()`, `list_resources()`, `list_resource_templates()`, and `list_prompts()` return the same lists and are still what the protocol handlers call, so subclass overrides still take effect. `add_tool()`, `add_resource()`, and `add_prompt()` are unchanged.
681+
- **Listing and registration methods.** `list_tools()`, `list_resources()`, `list_resource_templates()`, and `list_prompts()` return the same lists and are still what the protocol handlers call, so subclass overrides still take effect. `add_tool()`, `add_resource()`, and `add_prompt()` are unchanged (`add_prompt()` additionally accepts the plain function, like `add_tool()`).
682682
- **Helpers.** `Image.to_image_content()`, `Audio.to_audio_content()`, and the prompt `Message`, `UserMessage`, and `AssistantMessage` classes.
683683
- **Lifespan.** The `lifespan=` constructor argument and `ctx.request_context.lifespan_context` work as before, and the class is still generic over the lifespan result: `FastMCP[MyState]` becomes `MCPServer[MyState]`. (`Context`'s own type parameters did change; see [`RequestContext` type parameters simplified](#requestcontext-type-parameters-simplified).)
684684
- **Tool internals.** `Tool`, `Tool.from_function()`, `FuncMetadata`, `ArgModelBase`, and `func_metadata()` keep their v1 shapes; the one change is the now-required `context` argument to `Tool.run()`, described [below](#mcpservercall_tool-read_resource-get_prompt-now-accept-a-context-parameter).

src/mcp/server/mcpserver/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414

1515
from .context import Context
16+
from .prompts.base import AssistantMessage, Message, UserMessage
1617
from .resolve import (
1718
AcceptedElicitation,
1819
CancelledElicitation,
@@ -32,6 +33,9 @@
3233
"Context",
3334
"Image",
3435
"Audio",
36+
"Message",
37+
"UserMessage",
38+
"AssistantMessage",
3539
"Icon",
3640
"Resolve",
3741
"Elicit",

src/mcp/server/mcpserver/server.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -890,12 +890,47 @@ def decorator(fn: _CallableT) -> _CallableT:
890890

891891
return decorator
892892

893-
def add_prompt(self, prompt: Prompt) -> None:
893+
@overload
894+
def add_prompt(self, prompt: Prompt) -> None: ...
895+
896+
@overload
897+
def add_prompt(
898+
self,
899+
fn: Callable[..., Any],
900+
/,
901+
*,
902+
name: str | None = None,
903+
title: str | None = None,
904+
description: str | None = None,
905+
icons: list[Icon] | None = None,
906+
) -> None: ...
907+
908+
def add_prompt(
909+
self,
910+
prompt: Prompt | Callable[..., Any],
911+
*,
912+
name: str | None = None,
913+
title: str | None = None,
914+
description: str | None = None,
915+
icons: list[Icon] | None = None,
916+
) -> None:
894917
"""Add a prompt to the server.
895918
919+
Pass the function that renders the prompt: its name, docstring and parameters become
920+
the prompt's name, description and arguments, exactly as with `@prompt()`. A ready-made
921+
`Prompt` instance is registered as-is.
922+
896923
Args:
897-
prompt: A Prompt instance to add
924+
prompt: The function to register as a prompt, or a `Prompt` instance
925+
name: Optional name for the prompt (defaults to the function name)
926+
title: Optional human-readable title for the prompt
927+
description: Optional description (defaults to the function's docstring)
928+
icons: Optional list of icons for the prompt
898929
"""
930+
if not isinstance(prompt, Prompt):
931+
prompt = Prompt.from_function(prompt, name=name, title=title, description=description, icons=icons)
932+
elif any(arg is not None for arg in (name, title, description, icons)):
933+
raise TypeError("name, title, description and icons can only be set when registering a function")
899934
self._prompt_manager.add_prompt(prompt)
900935

901936
def remove_prompt(self, name: str) -> None:

tests/server/mcpserver/test_server.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from mcp.server.mcpserver import Context, MCPServer, ResourceSecurity
5151
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
5252
from mcp.server.mcpserver.prompts.base import Message, UserMessage
53+
from mcp.server.mcpserver.prompts.base import Prompt as PromptTemplate # `Prompt` here is the mcp_types wire model
5354
from mcp.server.mcpserver.resources import FileResource, FunctionResource
5455
from mcp.server.mcpserver.utilities.types import Audio, Image
5556
from mcp.server.subscriptions import (
@@ -2320,6 +2321,46 @@ def test_context_exposes_its_mcp_server() -> None:
23202321
assert Context(mcp_server=mcp).mcp_server is mcp
23212322

23222323

2324+
def _greet(who: str) -> str:
2325+
"""Say hi."""
2326+
return f"hi {who}"
2327+
2328+
2329+
async def test_add_prompt_registers_a_function_like_the_decorator() -> None:
2330+
"""SDK-defined: `add_prompt(fn, ...)` derives name, description and arguments as `@prompt()` does."""
2331+
mcp = MCPServer()
2332+
mcp.add_prompt(_greet, title="Greeter")
2333+
2334+
async with Client(mcp) as client:
2335+
[listed] = (await client.list_prompts()).prompts
2336+
result = await client.get_prompt("_greet", {"who": "max"})
2337+
2338+
assert (listed.name, listed.title, listed.description) == ("_greet", "Greeter", "Say hi.")
2339+
assert [arg.name for arg in listed.arguments or []] == ["who"]
2340+
assert result.messages[0].content == TextContent(type="text", text="hi max")
2341+
2342+
2343+
async def test_add_prompt_registers_a_prompt_instance_as_is() -> None:
2344+
"""SDK-defined: a ready-made prompt handed to `add_prompt` (the 2.0 form) is registered exactly as built."""
2345+
mcp = MCPServer()
2346+
mcp.add_prompt(prompt=PromptTemplate.from_function(_greet, name="custom"))
2347+
[listed] = await mcp.list_prompts()
2348+
assert listed.name == "custom"
2349+
2350+
2351+
async def test_add_prompt_rejects_overrides_alongside_a_prompt_instance() -> None:
2352+
"""SDK-defined: the keyword overrides only apply to the function form; passing them alongside a
2353+
ready-made prompt is rejected rather than silently ignored."""
2354+
mcp = MCPServer()
2355+
prompt: Any = PromptTemplate.from_function(_greet) # Any: the overloads already reject this call statically
2356+
with pytest.raises(TypeError) as exc_info:
2357+
mcp.add_prompt(prompt, name="renamed")
2358+
assert str(exc_info.value) == snapshot(
2359+
"name, title, description and icons can only be set when registering a function"
2360+
)
2361+
assert await mcp.list_prompts() == []
2362+
2363+
23232364
def test_remove_prompt_removes_and_unknown_name_raises() -> None:
23242365
mcp = MCPServer()
23252366

0 commit comments

Comments
 (0)