mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Ensure prompts return descriptions
This commit is contained in:
parent
2a6fb01038
commit
2eb0e7c1b3
15 changed files with 206 additions and 159 deletions
|
|
@ -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:
|
||||
|
||||
<Warning>
|
||||
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.
|
||||
</Warning>
|
||||
|
||||
#### 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.
|
||||
|
||||
<Warning>
|
||||
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.
|
||||
</Warning>
|
||||
|
||||
```python
|
||||
# Standard method - returns just the list of tools
|
||||
tools = await client.list_tools()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
]
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue