From 2eb0e7c1b39d84213f0b23530f93f08e35c7fb5f Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Mon, 5 May 2025 12:33:05 -0400
Subject: [PATCH] Ensure prompts return descriptions
---
docs/clients/client.mdx | 8 ++
docs/servers/prompts.mdx | 27 ++--
src/fastmcp/client/client.py | 7 +-
src/fastmcp/prompts/__init__.py | 9 +-
src/fastmcp/prompts/prompt.py | 65 +++++----
src/fastmcp/prompts/prompt_manager.py | 13 +-
src/fastmcp/server/proxy.py | 8 +-
src/fastmcp/server/server.py | 10 +-
tests/client/test_client.py | 16 ++-
.../prompts/{test_base.py => test_prompt.py} | 133 ++++++++++--------
tests/prompts/test_prompt_manager.py | 22 ++-
tests/server/test_import_server.py | 6 +-
tests/server/test_proxy.py | 8 +-
tests/server/test_server.py | 24 ++--
tests/server/test_server_interactions.py | 21 +--
15 files changed, 212 insertions(+), 165 deletions(-)
rename tests/prompts/{test_base.py => test_prompt.py} (62%)
diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx
index 5602887dc..feb393505 100644
--- a/docs/clients/client.mdx
+++ b/docs/clients/client.mdx
@@ -104,6 +104,10 @@ You can make multiple calls to the server within the same `async with` block usi
The `Client` provides methods corresponding to standard MCP requests:
+
+The standard client methods return user-friendly representations that may change as the protocol evolves. For consistent access to the complete data structure, use the `*_mcp` methods described later.
+
+
#### Tool Operations
* **`list_tools()`**: Retrieves a list of tools available on the server.
@@ -153,6 +157,10 @@ The `Client` provides methods corresponding to standard MCP requests:
The FastMCP client attempts to provide a "friendly" interface to the MCP protocol, but sometimes you may need access to the raw MCP protocol objects. Each of the main client methods that returns data has a corresponding `*_mcp` method that returns the raw MCP protocol objects directly.
+
+The standard client methods (without `_mcp`) return user-friendly representations of MCP data, while `*_mcp` methods will always return the complete MCP protocol objects. As the protocol evolves, changes to these user-friendly representations may occur and could potentially be breaking. If you need consistent, stable access to the full data structure, prefer using the `*_mcp` methods.
+
+
```python
# Standard method - returns just the list of tools
tools = await client.list_tools()
diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx
index 75d0418a5..f1e292dd6 100644
--- a/docs/servers/prompts.mdx
+++ b/docs/servers/prompts.mdx
@@ -28,11 +28,11 @@ The most common way to define a prompt is by decorating a Python function. The d
```python
from fastmcp import FastMCP
-from fastmcp.prompts.prompt import UserMessage, AssistantMessage, Message
+from fastmcp.prompts.prompt import Message, PromptMessage, TextContent
mcp = FastMCP(name="PromptServer")
-# Basic prompt returning a string (converted to UserMessage)
+# Basic prompt returning a string (converted to user message automatically)
@mcp.prompt()
def ask_about_topic(topic: str) -> str:
"""Generates a user message asking for an explanation of a topic."""
@@ -40,10 +40,10 @@ def ask_about_topic(topic: str) -> str:
# Prompt returning a specific message type
@mcp.prompt()
-def generate_code_request(language: str, task_description: str) -> UserMessage:
+def generate_code_request(language: str, task_description: str) -> PromptMessage:
"""Generates a user message requesting code generation."""
content = f"Write a {language} function that performs the following task: {task_description}"
- return UserMessage(content=content)
+ return PromptMessage(role="user", content=TextContent(type="text", text=content))
```
**Key Concepts:**
@@ -61,24 +61,21 @@ Functions with `*args` or `**kwargs` are not supported as prompts. This restrict
FastMCP intelligently handles different return types from your prompt function:
-- **`str`**: Automatically converted to a single `UserMessage`.
-- **`Message`** (e.g., `UserMessage`, `AssistantMessage`): Used directly as provided.
-- **`dict`**: Parsed as a `Message` object if it has the correct structure.
-- **`list[Message]`**: Used as a sequence of messages (a conversation).
+- **`str`**: Automatically converted to a single `PromptMessage`.
+- **`PromptMessage`**: Used directly as provided. (Note a more user-friendly `Message` constructor is available that can accept raw strings instead of `TextContent` objects.)
+- **`list[PromptMessage | str]`**: Used as a sequence of messages (a conversation).
+- **`Any`**: If the return type is not one of the above, the return value is attempted to be converted to a string and used as a `PromptMessage`.
```python
+from fastmcp.prompts.prompt import Message
+
@mcp.prompt()
def roleplay_scenario(character: str, situation: str) -> list[Message]:
"""Sets up a roleplaying scenario with initial messages."""
return [
- UserMessage(f"Let's roleplay. You are {character}. The situation is: {situation}"),
- AssistantMessage("Okay, I understand. I am ready. What happens next?")
+ Message(f"Let's roleplay. You are {character}. The situation is: {situation}"),
+ Message("Okay, I understand. I am ready. What happens next?", role="assistant")
]
-
-@mcp.prompt()
-def ask_for_feedback() -> dict:
- """Generates a user message asking for feedback."""
- return {"role": "user", "content": "What did you think of my previous response?"}
```
### Type Annotations
diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py
index 7ca6528d1..32536e13c 100644
--- a/src/fastmcp/client/client.py
+++ b/src/fastmcp/client/client.py
@@ -286,7 +286,7 @@ class Client:
async def get_prompt(
self, name: str, arguments: dict[str, str] | None = None
- ) -> list[mcp.types.PromptMessage]:
+ ) -> mcp.types.GetPromptResult:
"""Retrieve a rendered prompt message list from the server.
Args:
@@ -294,13 +294,14 @@ class Client:
arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
Returns:
- list[mcp.types.PromptMessage]: A list of prompt messages.
+ mcp.types.GetPromptResult: The complete response object from the protocol,
+ containing the prompt messages and any additional metadata.
Raises:
RuntimeError: If called while the client is not connected.
"""
result = await self.get_prompt_mcp(name=name, arguments=arguments)
- return result.messages
+ return result
# --- Completion ---
diff --git a/src/fastmcp/prompts/__init__.py b/src/fastmcp/prompts/__init__.py
index 21e29a0b4..1a8d91255 100644
--- a/src/fastmcp/prompts/__init__.py
+++ b/src/fastmcp/prompts/__init__.py
@@ -1,4 +1,9 @@
-from .prompt import Prompt, Message, UserMessage, AssistantMessage
+from .prompt import Prompt, PromptMessage, Message
from .prompt_manager import PromptManager
-__all__ = ["Prompt", "PromptManager", "Message", "UserMessage", "AssistantMessage"]
+__all__ = [
+ "Prompt",
+ "PromptManager",
+ "PromptMessage",
+ "Message",
+]
diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py
index 801bab62c..763c12021 100644
--- a/src/fastmcp/prompts/prompt.py
+++ b/src/fastmcp/prompts/prompt.py
@@ -4,10 +4,10 @@ from __future__ import annotations as _annotations
import inspect
from collections.abc import Awaitable, Callable, Sequence
-from typing import TYPE_CHECKING, Annotated, Any, Literal
+from typing import TYPE_CHECKING, Annotated, Any
import pydantic_core
-from mcp.types import EmbeddedResource, ImageContent, TextContent
+from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
from mcp.types import Prompt as MCPPrompt
from mcp.types import PromptArgument as MCPPromptArgument
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
@@ -28,32 +28,24 @@ if TYPE_CHECKING:
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
-class Message(BaseModel):
- """Base class for all prompt messages."""
-
- role: Literal["user", "assistant"]
- content: CONTENT_TYPES
-
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
- if isinstance(content, str):
- content = TextContent(type="text", text=content)
- super().__init__(content=content, **kwargs)
+def Message(
+ content: str | CONTENT_TYPES, role: Role | None = None, **kwargs: Any
+) -> PromptMessage:
+ """A user-friendly constructor for PromptMessage."""
+ if isinstance(content, str):
+ content = TextContent(type="text", text=content)
+ if role is None:
+ role = "user"
+ return PromptMessage(content=content, role=role, **kwargs)
-def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
- """A message from the user."""
- return Message(content=content, role="user", **kwargs)
-
-
-def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
- """A message from the assistant."""
- return Message(content=content, role="assistant", **kwargs)
-
-
-message_validator = TypeAdapter[Message](Message)
+message_validator = TypeAdapter[PromptMessage](PromptMessage)
SyncPromptResult = (
- str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
+ str
+ | PromptMessage
+ | dict[str, Any]
+ | Sequence[str | PromptMessage | dict[str, Any]]
)
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
@@ -156,7 +148,7 @@ class Prompt(BaseModel):
self,
arguments: dict[str, Any] | None = None,
context: Context[ServerSessionT, LifespanContextT] | None = None,
- ) -> list[Message]:
+ ) -> list[PromptMessage]:
"""Render the prompt with arguments."""
# Validate required arguments
if self.arguments:
@@ -182,21 +174,28 @@ class Prompt(BaseModel):
result = [result]
# Convert result to messages
- messages: list[Message] = []
- for msg in result: # type: ignore[reportUnknownVariableType]
+ messages: list[PromptMessage] = []
+ for msg in result:
try:
- if isinstance(msg, Message):
+ if isinstance(msg, PromptMessage):
messages.append(msg)
- elif isinstance(msg, dict):
- messages.append(message_validator.validate_python(msg))
elif isinstance(msg, str):
- content = TextContent(type="text", text=msg)
- messages.append(Message(role="user", content=content))
+ messages.append(
+ PromptMessage(
+ role="user",
+ content=TextContent(type="text", text=msg),
+ )
+ )
else:
content = pydantic_core.to_json(
msg, fallback=str, indent=2
).decode()
- messages.append(Message(role="user", content=content))
+ messages.append(
+ PromptMessage(
+ role="user",
+ content=TextContent(type="text", text=content),
+ )
+ )
except Exception:
raise ValueError(
f"Could not convert prompt result to message: {msg}"
diff --git a/src/fastmcp/prompts/prompt_manager.py b/src/fastmcp/prompts/prompt_manager.py
index db16d4398..4de06c03c 100644
--- a/src/fastmcp/prompts/prompt_manager.py
+++ b/src/fastmcp/prompts/prompt_manager.py
@@ -5,8 +5,10 @@ from __future__ import annotations as _annotations
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING, Any
+from mcp import GetPromptResult
+
from fastmcp.exceptions import NotFoundError
-from fastmcp.prompts.prompt import Message, Prompt, PromptResult
+from fastmcp.prompts.prompt import Prompt, PromptResult
from fastmcp.settings import DuplicateBehavior
from fastmcp.utilities.logging import get_logger
@@ -81,13 +83,18 @@ class PromptManager:
name: str,
arguments: dict[str, Any] | None = None,
context: Context[ServerSessionT, LifespanContextT] | None = None,
- ) -> list[Message]:
+ ) -> GetPromptResult:
"""Render a prompt by name with arguments."""
prompt = self.get_prompt(name)
if not prompt:
raise NotFoundError(f"Unknown prompt: {name}")
- return await prompt.render(arguments, context=context)
+ messages = await prompt.render(arguments, context=context)
+
+ return GetPromptResult(
+ description=prompt.description,
+ messages=messages,
+ )
def has_prompt(self, key: str) -> bool:
"""Check if a prompt exists."""
diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py
index a2f83fa4c..038623aab 100644
--- a/src/fastmcp/server/proxy.py
+++ b/src/fastmcp/server/proxy.py
@@ -19,7 +19,7 @@ from pydantic.networks import AnyUrl
from fastmcp.client import Client
from fastmcp.exceptions import NotFoundError
-from fastmcp.prompts import Message, Prompt
+from fastmcp.prompts import Prompt, PromptMessage
from fastmcp.resources import Resource, ResourceTemplate
from fastmcp.server.context import Context
from fastmcp.server.server import FastMCP
@@ -175,10 +175,10 @@ class ProxyPrompt(Prompt):
self,
arguments: dict[str, Any],
context: Context[ServerSessionT, LifespanContextT] | None = None,
- ) -> list[Message]:
+ ) -> list[PromptMessage]:
async with self._client:
result = await self._client.get_prompt(self.name, arguments)
- return [Message(role=m.role, content=m.content) for m in result]
+ return result.messages
class FastMCPProxy(FastMCP):
@@ -291,4 +291,4 @@ class FastMCPProxy(FastMCP):
except NotFoundError:
async with self.client:
result = await self.client.get_prompt(name, arguments)
- return GetPromptResult(messages=result)
+ return result
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 1de7dd908..a92d94f95 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -32,7 +32,6 @@ from mcp.types import (
EmbeddedResource,
GetPromptResult,
ImageContent,
- PromptMessage,
TextContent,
ToolAnnotations,
)
@@ -504,15 +503,10 @@ class FastMCP(Generic[LifespanResultT]):
"""
if self._prompt_manager.has_prompt(name):
context = self.get_context()
- messages = await self._prompt_manager.render_prompt(
+ prompt_result = await self._prompt_manager.render_prompt(
name, arguments=arguments or {}, context=context
)
-
- return GetPromptResult(
- messages=[
- PromptMessage(role=m.role, content=m.content) for m in messages
- ]
- )
+ return prompt_result
else:
for server in self._mounted_servers.values():
if server.match_prompt(name):
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index c6d671fc4..dcb1624cd 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -5,6 +5,7 @@ from pydantic import AnyUrl
from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport
+from fastmcp.prompts.prompt import TextContent
from fastmcp.server.server import FastMCP
@@ -38,6 +39,7 @@ def fastmcp_server():
# Add a prompt
@server.prompt()
def welcome(name: str) -> str:
+ """Example greeting prompt."""
return f"Welcome to FastMCP, {name}!"
return server
@@ -182,8 +184,9 @@ async def test_get_prompt(fastmcp_server):
result = await client.get_prompt("welcome", {"name": "Developer"})
# The result should contain our welcome message
- result_str = str(result)
- assert "Welcome to FastMCP, Developer!" in result_str
+ assert isinstance(result.messages[0].content, TextContent)
+ assert result.messages[0].content.text == "Welcome to FastMCP, Developer!"
+ assert result.description == "Example greeting prompt."
async def test_get_prompt_mcp(fastmcp_server):
@@ -193,11 +196,10 @@ async def test_get_prompt_mcp(fastmcp_server):
async with client:
result = await client.get_prompt_mcp("welcome", {"name": "Developer"})
- # Check that we got the raw MCP GetPromptResult object
- assert hasattr(result, "messages")
- assert len(result.messages) > 0
- result_str = str(result.messages)
- assert "Welcome to FastMCP, Developer!" in result_str
+ # The result should contain our welcome message
+ assert isinstance(result.messages[0].content, TextContent)
+ assert result.messages[0].content.text == "Welcome to FastMCP, Developer!"
+ assert result.description == "Example greeting prompt."
async def test_read_resource(fastmcp_server):
diff --git a/tests/prompts/test_base.py b/tests/prompts/test_prompt.py
similarity index 62%
rename from tests/prompts/test_base.py
rename to tests/prompts/test_prompt.py
index 24c05ccd8..c77ea0b88 100644
--- a/tests/prompts/test_base.py
+++ b/tests/prompts/test_prompt.py
@@ -3,11 +3,10 @@ from mcp.types import EmbeddedResource, TextResourceContents
from pydantic import FileUrl
from fastmcp.prompts.prompt import (
- AssistantMessage,
Message,
Prompt,
+ PromptMessage,
TextContent,
- UserMessage,
)
@@ -18,7 +17,9 @@ class TestRenderPrompt:
prompt = Prompt.from_function(fn)
assert await prompt.render() == [
- UserMessage(content=TextContent(type="text", text="Hello, world!"))
+ PromptMessage(
+ role="user", content=TextContent(type="text", text="Hello, world!")
+ )
]
async def test_async_fn(self):
@@ -27,7 +28,9 @@ class TestRenderPrompt:
prompt = Prompt.from_function(fn)
assert await prompt.render() == [
- UserMessage(content=TextContent(type="text", text="Hello, world!"))
+ PromptMessage(
+ role="user", content=TextContent(type="text", text="Hello, world!")
+ )
]
async def test_fn_with_args(self):
@@ -36,10 +39,11 @@ class TestRenderPrompt:
prompt = Prompt.from_function(fn)
assert await prompt.render(arguments=dict(name="World")) == [
- UserMessage(
+ PromptMessage(
+ role="user",
content=TextContent(
type="text", text="Hello, World! You're 30 years old."
- )
+ ),
)
]
@@ -52,33 +56,42 @@ class TestRenderPrompt:
await prompt.render(arguments=dict(age=40))
async def test_fn_returns_message(self):
- async def fn() -> Message:
- return UserMessage(content="Hello, world!")
-
- prompt = Prompt.from_function(fn)
- assert await prompt.render() == [
- UserMessage(content=TextContent(type="text", text="Hello, world!"))
- ]
-
- async def test_fn_returns_assistant_message(self):
- async def fn() -> Message:
- return AssistantMessage(
- content=TextContent(type="text", text="Hello, world!")
+ async def fn() -> PromptMessage:
+ return PromptMessage(
+ role="user", content=TextContent(type="text", text="Hello, world!")
)
prompt = Prompt.from_function(fn)
assert await prompt.render() == [
- AssistantMessage(content=TextContent(type="text", text="Hello, world!"))
+ PromptMessage(
+ role="user", content=TextContent(type="text", text="Hello, world!")
+ )
+ ]
+
+ async def test_fn_returns_assistant_message(self):
+ async def fn() -> PromptMessage:
+ return PromptMessage(
+ role="assistant", content=TextContent(type="text", text="Hello, world!")
+ )
+
+ prompt = Prompt.from_function(fn)
+ assert await prompt.render() == [
+ PromptMessage(
+ role="assistant", content=TextContent(type="text", text="Hello, world!")
+ )
]
async def test_fn_returns_multiple_messages(self):
expected = [
- UserMessage("Hello, world!"),
- AssistantMessage("How can I help you today?"),
- UserMessage("I'm looking for a restaurant in the center of town."),
+ Message(role="user", content="Hello, world!"),
+ Message(role="assistant", content="How can I help you today?"),
+ Message(
+ role="user",
+ content="I'm looking for a restaurant in the center of town.",
+ ),
]
- async def fn() -> list[Message]:
+ async def fn() -> list[PromptMessage]:
return expected
prompt = Prompt.from_function(fn)
@@ -94,13 +107,17 @@ class TestRenderPrompt:
return expected
prompt = Prompt.from_function(fn)
- assert await prompt.render() == [UserMessage(t) for t in expected]
+ assert await prompt.render() == [
+ PromptMessage(role="user", content=TextContent(type="text", text=t))
+ for t in expected
+ ]
async def test_fn_returns_resource_content(self):
"""Test returning a message with resource content."""
- async def fn() -> Message:
- return UserMessage(
+ async def fn() -> PromptMessage:
+ return PromptMessage(
+ role="user",
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
@@ -108,12 +125,13 @@ class TestRenderPrompt:
text="File contents",
mimeType="text/plain",
),
- )
+ ),
)
prompt = Prompt.from_function(fn)
assert await prompt.render() == [
- UserMessage(
+ PromptMessage(
+ role="user",
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
@@ -121,17 +139,18 @@ class TestRenderPrompt:
text="File contents",
mimeType="text/plain",
),
- )
+ ),
)
]
async def test_fn_returns_mixed_content(self):
"""Test returning messages with mixed content types."""
- async def fn() -> list[Message]:
+ async def fn() -> list[PromptMessage | str]:
return [
- UserMessage(content="Please analyze this file:"),
- UserMessage(
+ "Please analyze this file:",
+ PromptMessage(
+ role="user",
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
@@ -139,17 +158,19 @@ class TestRenderPrompt:
text="File contents",
mimeType="text/plain",
),
- )
+ ),
),
- AssistantMessage(content="I'll help analyze that file."),
+ Message(role="assistant", content="I'll help analyze that file."),
]
prompt = Prompt.from_function(fn)
assert await prompt.render() == [
- UserMessage(
- content=TextContent(type="text", text="Please analyze this file:")
+ PromptMessage(
+ role="user",
+ content=TextContent(type="text", text="Please analyze this file:"),
),
- UserMessage(
+ PromptMessage(
+ role="user",
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
@@ -157,32 +178,34 @@ class TestRenderPrompt:
text="File contents",
mimeType="text/plain",
),
- )
+ ),
),
- AssistantMessage(
- content=TextContent(type="text", text="I'll help analyze that file.")
+ PromptMessage(
+ role="assistant",
+ content=TextContent(type="text", text="I'll help analyze that file."),
),
]
- async def test_fn_returns_dict_with_resource(self):
+ async def test_fn_returns_message_with_resource(self):
"""Test returning a dict with resource content."""
- async def fn() -> dict:
- return {
- "role": "user",
- "content": {
- "type": "resource",
- "resource": {
- "uri": FileUrl("file://file.txt"),
- "text": "File contents",
- "mimeType": "text/plain",
- },
- },
- }
+ async def fn() -> PromptMessage:
+ return PromptMessage(
+ role="user",
+ content=EmbeddedResource(
+ type="resource",
+ resource=TextResourceContents(
+ uri=FileUrl("file://file.txt"),
+ text="File contents",
+ mimeType="text/plain",
+ ),
+ ),
+ )
prompt = Prompt.from_function(fn)
assert await prompt.render() == [
- UserMessage(
+ PromptMessage(
+ role="user",
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
@@ -190,6 +213,6 @@ class TestRenderPrompt:
text="File contents",
mimeType="text/plain",
),
- )
+ ),
)
]
diff --git a/tests/prompts/test_prompt_manager.py b/tests/prompts/test_prompt_manager.py
index c00d27e49..3a41003e7 100644
--- a/tests/prompts/test_prompt_manager.py
+++ b/tests/prompts/test_prompt_manager.py
@@ -7,7 +7,7 @@ from mcp.shared.context import LifespanContextT
from fastmcp import Context
from fastmcp.exceptions import NotFoundError
from fastmcp.prompts import Prompt
-from fastmcp.prompts.prompt import TextContent, UserMessage
+from fastmcp.prompts.prompt import PromptMessage, TextContent
from fastmcp.prompts.prompt_manager import PromptManager
@@ -147,28 +147,36 @@ class TestPromptManager:
"""Test rendering a prompt."""
def fn() -> str:
+ """An example prompt."""
return "Hello, world!"
manager = PromptManager()
prompt = Prompt.from_function(fn)
manager.add_prompt(prompt)
- messages = await manager.render_prompt("fn")
- assert messages == [
- UserMessage(content=TextContent(type="text", text="Hello, world!"))
+ result = await manager.render_prompt("fn")
+ assert result.description == "An example prompt."
+ assert result.messages == [
+ PromptMessage(
+ role="user", content=TextContent(type="text", text="Hello, world!")
+ )
]
async def test_render_prompt_with_args(self):
"""Test rendering a prompt with arguments."""
def fn(name: str) -> str:
+ """An example prompt."""
return f"Hello, {name}!"
manager = PromptManager()
prompt = Prompt.from_function(fn)
manager.add_prompt(prompt)
- messages = await manager.render_prompt("fn", arguments={"name": "World"})
- assert messages == [
- UserMessage(content=TextContent(type="text", text="Hello, World!"))
+ result = await manager.render_prompt("fn", arguments={"name": "World"})
+ assert result.description == "An example prompt."
+ assert result.messages == [
+ PromptMessage(
+ role="user", content=TextContent(type="text", text="Hello, World!")
+ )
]
async def test_render_unknown_prompt(self):
diff --git a/tests/server/test_import_server.py b/tests/server/test_import_server.py
index c9160e4b1..a122ee23c 100644
--- a/tests/server/test_import_server.py
+++ b/tests/server/test_import_server.py
@@ -319,14 +319,16 @@ async def test_import_with_proxy_prompts():
@api_app.prompt()
def greeting(name: str) -> str:
+ """Example greeting prompt."""
return f"Hello, {name} from API!"
proxy_app = FastMCP.from_client(Client(api_app))
await main_app.import_server("api", proxy_app)
result = await main_app._mcp_get_prompt("api_greeting", {"name": "World"})
- assert result.messages is not None
- # Check that the message contains our greeting
+ assert isinstance(result.messages[0].content, TextContent)
+ assert result.messages[0].content.text == "Hello, World from API!"
+ assert result.description == "Example greeting prompt."
async def test_import_with_proxy_resources():
diff --git a/tests/server/test_proxy.py b/tests/server/test_proxy.py
index 882977527..aef2c0583 100644
--- a/tests/server/test_proxy.py
+++ b/tests/server/test_proxy.py
@@ -207,7 +207,7 @@ class TestPrompts:
async def test_render_prompt_calls_prompt(self, proxy_server):
async with Client(proxy_server) as client:
result = await client.get_prompt("welcome", {"name": "Alice"})
- assert isinstance(result[0], mcp.types.PromptMessage)
- assert result[0].role == "user"
- assert isinstance(result[0].content, mcp.types.TextContent)
- assert result[0].content.text == "Welcome to FastMCP, Alice!"
+ assert isinstance(result.messages[0], mcp.types.PromptMessage)
+ assert result.messages[0].role == "user"
+ assert isinstance(result.messages[0].content, mcp.types.TextContent)
+ assert result.messages[0].content.text == "Welcome to FastMCP, Alice!"
diff --git a/tests/server/test_server.py b/tests/server/test_server.py
index 6a80c03ab..ead1dd673 100644
--- a/tests/server/test_server.py
+++ b/tests/server/test_server.py
@@ -640,16 +640,16 @@ class TestPromptDecorator:
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt", {"name": "World"})
- assert len(result) == 1
- message = result[0]
+ assert len(result.messages) == 1
+ message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Hello, World!"
result = await client.get_prompt(
"test_prompt", {"name": "World", "greeting": "Hi"}
)
- assert len(result) == 1
- message = result[0]
+ assert len(result.messages) == 1
+ message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Hi, World!"
@@ -668,8 +668,8 @@ class TestPromptDecorator:
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
- assert len(result) == 1
- message = result[0]
+ assert len(result.messages) == 1
+ message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "My prefix: Hello, world!"
@@ -687,8 +687,8 @@ class TestPromptDecorator:
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
- assert len(result) == 1
- message = result[0]
+ assert len(result.messages) == 1
+ message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Class prefix: Hello, world!"
@@ -703,8 +703,8 @@ class TestPromptDecorator:
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
- assert len(result) == 1
- message = result[0]
+ assert len(result.messages) == 1
+ message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Static Hello, world!"
@@ -717,8 +717,8 @@ class TestPromptDecorator:
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
- assert len(result) == 1
- message = result[0]
+ assert len(result.messages) == 1
+ message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Async Hello, world!"
diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py
index 3489bf86c..1b95e20b7 100644
--- a/tests/server/test_server_interactions.py
+++ b/tests/server/test_server_interactions.py
@@ -18,7 +18,7 @@ from pydantic import AnyUrl, Field
from fastmcp import Client, Context, FastMCP
from fastmcp.exceptions import ClientError
-from fastmcp.prompts.prompt import EmbeddedResource, Message, UserMessage
+from fastmcp.prompts.prompt import EmbeddedResource, PromptMessage
from fastmcp.resources import FileResource, FunctionResource
from fastmcp.utilities.types import Image
@@ -1267,8 +1267,8 @@ class TestPrompts:
async with Client(mcp) as client:
result = await client.get_prompt("fn", {"name": "World"})
- assert len(result) == 1
- message = result[0]
+ assert len(result.messages) == 1
+ message = result.messages[0]
assert message.role == "user"
content = message.content
assert isinstance(content, TextContent)
@@ -1279,8 +1279,9 @@ class TestPrompts:
mcp = FastMCP()
@mcp.prompt()
- def fn() -> Message:
- return UserMessage(
+ def fn() -> PromptMessage:
+ return PromptMessage(
+ role="user",
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
@@ -1288,13 +1289,13 @@ class TestPrompts:
text="File contents",
mimeType="text/plain",
),
- )
+ ),
)
async with Client(mcp) as client:
result = await client.get_prompt("fn")
- assert result[0].role == "user"
- content = result[0].content
+ assert result.messages[0].role == "user"
+ content = result.messages[0].content
assert isinstance(content, EmbeddedResource)
resource = content.resource
assert isinstance(resource, TextResourceContents)
@@ -1370,6 +1371,6 @@ class TestPromptContext:
async with Client(mcp) as client:
result = await client.get_prompt("prompt_fn", {"name": "World"})
- assert len(result) == 1
- message = result[0]
+ assert len(result.messages) == 1
+ message = result.messages[0]
assert message.role == "user"