mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
This reverts commit f9e29cf58e.
This commit is contained in:
parent
5f3bb05f99
commit
8abd84bf1e
13 changed files with 234 additions and 82 deletions
|
|
@ -11,7 +11,7 @@ FastMCP is a comprehensive Python framework (Python ≥3.10) for building Model
|
|||
```bash
|
||||
uv sync # Install dependencies
|
||||
uv run prek run --all-files # Ruff + Prettier + ty
|
||||
uv run pytest # Run full test suite
|
||||
uv run pytest -n auto # Run full test suite
|
||||
```
|
||||
|
||||
**All three must pass** - this is enforced by CI. Alternative: `just build && just typecheck && just test`
|
||||
|
|
@ -242,9 +242,9 @@ uv sync # Installs all deps including dev tools
|
|||
|
||||
### Testing
|
||||
|
||||
- **Standard**: `uv run pytest`
|
||||
- **Integration**: `uv run pytest -m "integration"`
|
||||
- **Excluding markers**: `uv run pytest -m "not integration and not client_process"`
|
||||
- **Standard**: `uv run pytest -n auto`
|
||||
- **Integration**: `uv run pytest -n auto -m "integration"`
|
||||
- **Excluding markers**: `uv run pytest -n auto -m "not integration and not client_process"`
|
||||
|
||||
### CLI Usage
|
||||
|
||||
|
|
|
|||
|
|
@ -195,13 +195,14 @@ FastMCP intelligently handles different return types from your prompt function:
|
|||
- **`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).
|
||||
- **`PromptResult`**: Full control over messages, description, and metadata. See [PromptResult](#promptresult) below.
|
||||
- **`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, PromptResult
|
||||
from fastmcp.prompts.prompt import Message
|
||||
|
||||
@mcp.prompt
|
||||
def roleplay_scenario(character: str, situation: str) -> PromptResult:
|
||||
def roleplay_scenario(character: str, situation: str) -> list[Message]:
|
||||
"""Sets up a roleplaying scenario with initial messages."""
|
||||
return [
|
||||
Message(f"Let's roleplay. You are {character}. The situation is: {situation}"),
|
||||
|
|
@ -209,6 +210,43 @@ def roleplay_scenario(character: str, situation: str) -> PromptResult:
|
|||
]
|
||||
```
|
||||
|
||||
#### PromptResult
|
||||
|
||||
<VersionBadge version="2.14.1" />
|
||||
|
||||
For complete control over prompt responses, return a `PromptResult` object. This lets you include metadata alongside your prompt messages, which is useful for passing runtime information to clients.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.prompts import PromptResult, Message
|
||||
|
||||
mcp = FastMCP(name="PromptServer")
|
||||
|
||||
@mcp.prompt
|
||||
def code_review(code: str) -> PromptResult:
|
||||
"""Returns a code review prompt with metadata."""
|
||||
return PromptResult(
|
||||
messages=[
|
||||
Message(f"Please review this code:\n\n```\n{code}\n```"),
|
||||
],
|
||||
description="Code review prompt",
|
||||
meta={"review_type": "security", "priority": "high"}
|
||||
)
|
||||
```
|
||||
|
||||
`PromptResult` accepts three fields:
|
||||
|
||||
**`messages`** - A list of `PromptMessage` or `Message` objects representing the conversation to send to the LLM.
|
||||
|
||||
**`description`** - Optional description of the prompt result. If not provided, defaults to the prompt's docstring.
|
||||
|
||||
**`meta`** - Optional metadata dictionary that will be included in the MCP response's `_meta` field. Use this for runtime metadata like categorization, priority, or other client-specific data.
|
||||
|
||||
<Note>
|
||||
The `meta` field in `PromptResult` is for runtime metadata specific to this render response. This is separate from the `meta` parameter in `@mcp.prompt(meta={...})`, which provides static metadata about the prompt definition itself (returned when listing prompts).
|
||||
</Note>
|
||||
|
||||
You can still return plain `str`, `PromptMessage`, or lists from your prompt functions—`PromptResult` is opt-in for when you need to include metadata.
|
||||
|
||||
### Required vs. Optional Parameters
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .prompt import Prompt, PromptMessage, Message
|
||||
from .prompt import Message, Prompt, PromptResult, PromptMessage
|
||||
from .prompt_manager import PromptManager
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -6,4 +6,5 @@ __all__ = [
|
|||
"Prompt",
|
||||
"PromptManager",
|
||||
"PromptMessage",
|
||||
"PromptResult",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ from __future__ import annotations as _annotations
|
|||
|
||||
import inspect
|
||||
import json
|
||||
import warnings
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from typing import Annotated, Any
|
||||
|
||||
import pydantic_core
|
||||
from mcp import GetPromptResult
|
||||
from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
|
||||
from mcp.types import Prompt as SDKPrompt
|
||||
from mcp.types import PromptArgument as SDKPromptArgument
|
||||
from pydantic import Field, TypeAdapter
|
||||
|
||||
from fastmcp import settings
|
||||
from fastmcp.exceptions import PromptError
|
||||
from fastmcp.server.dependencies import get_context, without_injected_parameters
|
||||
from fastmcp.server.tasks.config import TaskConfig
|
||||
|
|
@ -40,13 +43,14 @@ def Message(
|
|||
|
||||
message_validator = TypeAdapter[PromptMessage](PromptMessage)
|
||||
|
||||
SyncPromptResult = (
|
||||
# Type aliases for what prompt functions can return (before conversion to PromptResult)
|
||||
_SyncPromptFnReturn = (
|
||||
str
|
||||
| PromptMessage
|
||||
| dict[str, Any]
|
||||
| Sequence[str | PromptMessage | dict[str, Any]]
|
||||
)
|
||||
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
|
||||
_PromptFnReturn = _SyncPromptFnReturn | Awaitable[_SyncPromptFnReturn]
|
||||
|
||||
|
||||
class PromptArgument(FastMCPBaseModel):
|
||||
|
|
@ -61,6 +65,51 @@ class PromptArgument(FastMCPBaseModel):
|
|||
)
|
||||
|
||||
|
||||
class PromptResult(FastMCPBaseModel):
|
||||
"""Canonical result type for prompt rendering.
|
||||
|
||||
This is the internal type that all prompt renders return. It wraps the
|
||||
messages with optional description and metadata.
|
||||
"""
|
||||
|
||||
messages: list[PromptMessage] = Field(description="The prompt messages to return")
|
||||
description: str | None = Field(
|
||||
default=None, description="Optional description of the prompt result"
|
||||
)
|
||||
meta: dict[str, Any] | None = Field(
|
||||
default=None, description="Optional metadata about the prompt result"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_value(
|
||||
cls,
|
||||
value: list[PromptMessage] | PromptResult,
|
||||
description: str | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
) -> PromptResult:
|
||||
"""Convert various types to PromptResult."""
|
||||
if isinstance(value, PromptResult):
|
||||
# Merge meta if provided
|
||||
if meta and value.meta:
|
||||
merged_meta = {**value.meta, **meta}
|
||||
else:
|
||||
merged_meta = meta or value.meta
|
||||
return cls(
|
||||
messages=value.messages,
|
||||
description=description or value.description,
|
||||
meta=merged_meta,
|
||||
)
|
||||
return cls(messages=value, description=description, meta=meta)
|
||||
|
||||
def to_mcp_prompt_result(self) -> GetPromptResult:
|
||||
"""Convert to MCP GetPromptResult."""
|
||||
return GetPromptResult(
|
||||
description=self.description,
|
||||
messages=self.messages,
|
||||
_meta=self.meta,
|
||||
)
|
||||
|
||||
|
||||
class Prompt(FastMCPComponent):
|
||||
"""A prompt template that can be rendered with parameters."""
|
||||
|
||||
|
|
@ -113,7 +162,7 @@ class Prompt(FastMCPComponent):
|
|||
|
||||
@staticmethod
|
||||
def from_function(
|
||||
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
||||
fn: Callable[..., _PromptFnReturn | Awaitable[_PromptFnReturn]],
|
||||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
|
|
@ -146,19 +195,45 @@ class Prompt(FastMCPComponent):
|
|||
async def render(
|
||||
self,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
) -> list[PromptMessage]:
|
||||
) -> list[PromptMessage] | PromptResult:
|
||||
"""Render the prompt with arguments.
|
||||
|
||||
This method is not implemented in the base Prompt class and must be
|
||||
implemented by subclasses.
|
||||
implemented by subclasses. The preferred return type is PromptResult,
|
||||
but list[PromptMessage] is still supported for backwards compatibility.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement render()")
|
||||
|
||||
async def _render(
|
||||
self,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
) -> PromptResult:
|
||||
"""Internal API that always returns PromptResult.
|
||||
|
||||
Calls render() and wraps list[PromptMessage] in PromptResult.
|
||||
This is what PromptManager calls internally.
|
||||
"""
|
||||
result = await self.render(arguments)
|
||||
if isinstance(result, PromptResult):
|
||||
return result
|
||||
# Deprecated in 2.14.1: returning list[PromptMessage] from render()
|
||||
if settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
f"Prompt.render() returning list[PromptMessage] is deprecated (since 2.14.1). "
|
||||
f"Return PromptResult instead. "
|
||||
f"(Prompt: {self.__class__.__name__}, Name: {self.name})",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return PromptResult.from_value(
|
||||
result, description=self.description, meta=self.meta
|
||||
)
|
||||
|
||||
|
||||
class FunctionPrompt(Prompt):
|
||||
"""A prompt that is a function."""
|
||||
|
||||
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
||||
fn: Callable[..., _PromptFnReturn | Awaitable[_PromptFnReturn]]
|
||||
task_config: Annotated[
|
||||
TaskConfig,
|
||||
Field(description="Background task execution configuration (SEP-1686)."),
|
||||
|
|
@ -167,7 +242,7 @@ class FunctionPrompt(Prompt):
|
|||
@classmethod
|
||||
def from_function(
|
||||
cls,
|
||||
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
||||
fn: Callable[..., _PromptFnReturn | Awaitable[_PromptFnReturn]],
|
||||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
|
|
@ -322,7 +397,7 @@ class FunctionPrompt(Prompt):
|
|||
async def render(
|
||||
self,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
) -> list[PromptMessage]:
|
||||
) -> PromptResult:
|
||||
"""Render the prompt with arguments."""
|
||||
# Validate required arguments
|
||||
if self.arguments:
|
||||
|
|
@ -375,7 +450,11 @@ class FunctionPrompt(Prompt):
|
|||
"Could not convert prompt result to message."
|
||||
) from e
|
||||
|
||||
return messages
|
||||
return PromptResult(
|
||||
messages=messages,
|
||||
description=self.description,
|
||||
meta=self.meta,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error rendering prompt {self.name}")
|
||||
raise PromptError(f"Error rendering prompt {self.name}.") from e
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import warnings
|
|||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from mcp import GetPromptResult
|
||||
|
||||
from fastmcp import settings
|
||||
from fastmcp.exceptions import NotFoundError, PromptError
|
||||
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
||||
from fastmcp.prompts.prompt import (
|
||||
FunctionPrompt,
|
||||
Prompt,
|
||||
PromptResult,
|
||||
_PromptFnReturn,
|
||||
)
|
||||
from fastmcp.settings import DuplicateBehavior
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
|
|
@ -24,7 +27,11 @@ class PromptManager:
|
|||
mask_error_details: bool | None = None,
|
||||
):
|
||||
self._prompts: dict[str, Prompt] = {}
|
||||
self.mask_error_details = mask_error_details or settings.mask_error_details
|
||||
self.mask_error_details = (
|
||||
settings.mask_error_details
|
||||
if mask_error_details is None
|
||||
else mask_error_details
|
||||
)
|
||||
|
||||
# Default to "warn" if None is provided
|
||||
if duplicate_behavior is None:
|
||||
|
|
@ -58,7 +65,7 @@ class PromptManager:
|
|||
|
||||
def add_prompt_from_fn(
|
||||
self,
|
||||
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
||||
fn: Callable[..., _PromptFnReturn | Awaitable[_PromptFnReturn]],
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
tags: set[str] | None = None,
|
||||
|
|
@ -98,18 +105,17 @@ class PromptManager:
|
|||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
) -> GetPromptResult:
|
||||
) -> PromptResult:
|
||||
"""
|
||||
Internal API for servers: Finds and renders a prompt, respecting the
|
||||
filtered protocol path.
|
||||
"""
|
||||
prompt = await self.get_prompt(name)
|
||||
try:
|
||||
messages = await prompt.render(arguments)
|
||||
return GetPromptResult(description=prompt.description, messages=messages)
|
||||
except PromptError as e:
|
||||
return await prompt._render(arguments)
|
||||
except PromptError:
|
||||
logger.exception(f"Error rendering prompt {name!r}")
|
||||
raise e
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Error rendering prompt {name!r}")
|
||||
if self.mask_error_details:
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from key_value.aio.wrappers.statistics.wrapper import (
|
|||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import NotRequired, Self, override
|
||||
|
||||
from fastmcp.prompts.prompt import Prompt
|
||||
from fastmcp.prompts.prompt import Prompt, PromptResult
|
||||
from fastmcp.resources.resource import Resource, ResourceContent
|
||||
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
|
||||
from fastmcp.tools.tool import Tool, ToolResult
|
||||
|
|
@ -220,12 +220,10 @@ class ResponseCachingMiddleware(Middleware):
|
|||
default_collection="resources/read",
|
||||
)
|
||||
|
||||
self._get_prompt_cache: PydanticAdapter[mcp.types.GetPromptResult] = (
|
||||
PydanticAdapter(
|
||||
key_value=self._stats,
|
||||
pydantic_model=mcp.types.GetPromptResult,
|
||||
default_collection="prompts/get",
|
||||
)
|
||||
self._get_prompt_cache: PydanticAdapter[PromptResult] = PydanticAdapter(
|
||||
key_value=self._stats,
|
||||
pydantic_model=PromptResult,
|
||||
default_collection="prompts/get",
|
||||
)
|
||||
|
||||
self._call_tool_cache: PydanticAdapter[CachableToolResult] = PydanticAdapter(
|
||||
|
|
@ -419,10 +417,8 @@ class ResponseCachingMiddleware(Middleware):
|
|||
async def on_get_prompt(
|
||||
self,
|
||||
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
||||
call_next: CallNext[
|
||||
mcp.types.GetPromptRequestParams, mcp.types.GetPromptResult
|
||||
],
|
||||
) -> mcp.types.GetPromptResult:
|
||||
call_next: CallNext[mcp.types.GetPromptRequestParams, PromptResult],
|
||||
) -> PromptResult:
|
||||
"""Get a prompt from the cache, if caching is enabled, and the result is in the cache. Otherwise,
|
||||
otherwise call the next middleware and store the result in the cache if caching is enabled."""
|
||||
if self._get_prompt_settings.get("enabled") is False:
|
||||
|
|
@ -433,7 +429,7 @@ class ResponseCachingMiddleware(Middleware):
|
|||
if cached_value := await self._get_prompt_cache.get(key=cache_key):
|
||||
return cached_value
|
||||
|
||||
value: mcp.types.GetPromptResult = await call_next(context=context)
|
||||
value: PromptResult = await call_next(context=context)
|
||||
|
||||
await self._get_prompt_cache.put(
|
||||
key=cache_key,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from typing import (
|
|||
import mcp.types as mt
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from fastmcp.prompts.prompt import Prompt
|
||||
from fastmcp.prompts.prompt import Prompt, PromptResult
|
||||
from fastmcp.resources.resource import Resource, ResourceContent
|
||||
from fastmcp.resources.template import ResourceTemplate
|
||||
from fastmcp.tools.tool import Tool, ToolResult
|
||||
|
|
@ -170,8 +170,8 @@ class Middleware:
|
|||
async def on_get_prompt(
|
||||
self,
|
||||
context: MiddlewareContext[mt.GetPromptRequestParams],
|
||||
call_next: CallNext[mt.GetPromptRequestParams, mt.GetPromptResult],
|
||||
) -> mt.GetPromptResult:
|
||||
call_next: CallNext[mt.GetPromptRequestParams, PromptResult],
|
||||
) -> PromptResult:
|
||||
return await call_next(context)
|
||||
|
||||
async def on_list_tools(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ from mcp.types import (
|
|||
METHOD_NOT_FOUND,
|
||||
BlobResourceContents,
|
||||
ElicitRequestFormParams,
|
||||
GetPromptResult,
|
||||
TextResourceContents,
|
||||
)
|
||||
from pydantic.networks import AnyUrl
|
||||
|
|
@ -28,7 +27,7 @@ from fastmcp.client.roots import RootsList
|
|||
from fastmcp.client.transports import ClientTransportT
|
||||
from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
|
||||
from fastmcp.mcp_config import MCPConfig
|
||||
from fastmcp.prompts import Prompt, PromptMessage
|
||||
from fastmcp.prompts import Prompt, PromptResult
|
||||
from fastmcp.prompts.prompt import PromptArgument
|
||||
from fastmcp.prompts.prompt_manager import PromptManager
|
||||
from fastmcp.resources import Resource, ResourceTemplate
|
||||
|
|
@ -257,7 +256,7 @@ class ProxyPromptManager(PromptManager, ProxyManagerMixin):
|
|||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
) -> GetPromptResult:
|
||||
) -> PromptResult:
|
||||
"""Renders a prompt, trying local/mounted first, then proxy if not found."""
|
||||
try:
|
||||
# First try local and mounted prompts
|
||||
|
|
@ -267,7 +266,12 @@ class ProxyPromptManager(PromptManager, ProxyManagerMixin):
|
|||
client = await self._get_client()
|
||||
async with client:
|
||||
result = await client.get_prompt(name, arguments)
|
||||
return result
|
||||
# Convert MCP GetPromptResult to PromptResult
|
||||
return PromptResult(
|
||||
messages=result.messages,
|
||||
description=result.description,
|
||||
meta=result.meta,
|
||||
)
|
||||
|
||||
|
||||
class ProxyTool(Tool, MirroredComponent):
|
||||
|
|
@ -524,11 +528,17 @@ class ProxyPrompt(Prompt, MirroredComponent):
|
|||
_mirrored=True,
|
||||
)
|
||||
|
||||
async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]: # type: ignore[override]
|
||||
async def render(self, arguments: dict[str, Any]) -> PromptResult: # type: ignore[override]
|
||||
"""Render the prompt by making a call through the client."""
|
||||
async with self._client:
|
||||
result = await self._client.get_prompt(self.name, arguments)
|
||||
return result.messages
|
||||
# Convert GetPromptResult to PromptResult, preserving runtime meta from the result
|
||||
# (not the static prompt meta which includes fastmcp tags)
|
||||
return PromptResult(
|
||||
messages=result.messages,
|
||||
description=result.description,
|
||||
meta=result.meta,
|
||||
)
|
||||
|
||||
|
||||
class FastMCPProxy(FastMCP):
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ import fastmcp.server
|
|||
from fastmcp.exceptions import DisabledError, NotFoundError
|
||||
from fastmcp.mcp_config import MCPConfig
|
||||
from fastmcp.prompts import Prompt
|
||||
from fastmcp.prompts.prompt import FunctionPrompt
|
||||
from fastmcp.prompts.prompt import FunctionPrompt, PromptResult
|
||||
from fastmcp.prompts.prompt_manager import PromptManager
|
||||
from fastmcp.resources.resource import FunctionResource, Resource, ResourceContent
|
||||
from fastmcp.resources.resource_manager import ResourceManager
|
||||
|
|
@ -1781,8 +1781,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
) -> GetPromptResult:
|
||||
"""
|
||||
Applies this server's middleware and delegates the filtered call to the manager.
|
||||
Converts PromptResult to GetPromptResult for MCP protocol.
|
||||
"""
|
||||
result = await self._get_prompt_content_middleware(name, arguments)
|
||||
return result.to_mcp_prompt_result()
|
||||
|
||||
async def _get_prompt_content_middleware(
|
||||
self, name: str, arguments: dict[str, Any] | None = None
|
||||
) -> PromptResult:
|
||||
"""
|
||||
Applies this server's middleware and returns PromptResult.
|
||||
Used internally and by parent servers for mounted prompts.
|
||||
"""
|
||||
mw_context = MiddlewareContext(
|
||||
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
||||
source="client",
|
||||
|
|
@ -1797,7 +1807,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
async def _get_prompt(
|
||||
self,
|
||||
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
||||
) -> GetPromptResult:
|
||||
) -> PromptResult:
|
||||
name = context.message.name
|
||||
|
||||
# Try mounted servers in reverse order (later wins)
|
||||
|
|
@ -1816,7 +1826,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
if not self._should_enable_component(prompt):
|
||||
# Parent filter blocks this prompt, continue searching
|
||||
continue
|
||||
return await mounted.server._get_prompt_middleware(
|
||||
return await mounted.server._get_prompt_content_middleware(
|
||||
try_name, context.message.arguments
|
||||
)
|
||||
except NotFoundError:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ class TestRenderPrompt:
|
|||
return "Hello, world!"
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == [
|
||||
result = await prompt.render()
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text="Hello, world!")
|
||||
)
|
||||
|
|
@ -27,7 +28,8 @@ class TestRenderPrompt:
|
|||
return "Hello, world!"
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == [
|
||||
result = await prompt.render()
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text="Hello, world!")
|
||||
)
|
||||
|
|
@ -38,7 +40,8 @@ class TestRenderPrompt:
|
|||
return f"Hello, {name}! You're {age} years old."
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render(arguments=dict(name="World")) == [
|
||||
result = await prompt.render(arguments=dict(name="World"))
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user",
|
||||
content=TextContent(
|
||||
|
|
@ -53,7 +56,8 @@ class TestRenderPrompt:
|
|||
return f"Hello, {name}!"
|
||||
|
||||
prompt = Prompt.from_function(MyPrompt())
|
||||
assert await prompt.render(arguments=dict(name="World")) == [
|
||||
result = await prompt.render(arguments=dict(name="World"))
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text="Hello, World!")
|
||||
)
|
||||
|
|
@ -65,7 +69,8 @@ class TestRenderPrompt:
|
|||
return f"Hello, {name}!"
|
||||
|
||||
prompt = Prompt.from_function(MyPrompt())
|
||||
assert await prompt.render(arguments=dict(name="World")) == [
|
||||
result = await prompt.render(arguments=dict(name="World"))
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text="Hello, World!")
|
||||
)
|
||||
|
|
@ -86,7 +91,8 @@ class TestRenderPrompt:
|
|||
)
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == [
|
||||
result = await prompt.render()
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text="Hello, world!")
|
||||
)
|
||||
|
|
@ -99,7 +105,8 @@ class TestRenderPrompt:
|
|||
)
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == [
|
||||
result = await prompt.render()
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="assistant", content=TextContent(type="text", text="Hello, world!")
|
||||
)
|
||||
|
|
@ -119,7 +126,8 @@ class TestRenderPrompt:
|
|||
return expected
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == expected
|
||||
result = await prompt.render()
|
||||
assert result.messages == expected
|
||||
|
||||
async def test_fn_returns_list_of_strings(self):
|
||||
expected = [
|
||||
|
|
@ -131,7 +139,8 @@ class TestRenderPrompt:
|
|||
return expected
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == [
|
||||
result = await prompt.render()
|
||||
assert result.messages == [
|
||||
PromptMessage(role="user", content=TextContent(type="text", text=t))
|
||||
for t in expected
|
||||
]
|
||||
|
|
@ -153,7 +162,8 @@ class TestRenderPrompt:
|
|||
)
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == [
|
||||
result = await prompt.render()
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user",
|
||||
content=EmbeddedResource(
|
||||
|
|
@ -188,7 +198,8 @@ class TestRenderPrompt:
|
|||
]
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == [
|
||||
result = await prompt.render()
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user",
|
||||
content=TextContent(type="text", text="Please analyze this file:"),
|
||||
|
|
@ -227,7 +238,8 @@ class TestRenderPrompt:
|
|||
)
|
||||
|
||||
prompt = Prompt.from_function(fn)
|
||||
assert await prompt.render() == [
|
||||
result = await prompt.render()
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user",
|
||||
content=EmbeddedResource(
|
||||
|
|
@ -258,7 +270,7 @@ class TestPromptTypeConversion:
|
|||
result_from_string = await prompt.render(
|
||||
arguments={"numbers": "[1, 2, 3, 4, 5]"}
|
||||
)
|
||||
assert result_from_string == [
|
||||
assert result_from_string.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text="The sum is: 15")
|
||||
)
|
||||
|
|
@ -268,7 +280,7 @@ class TestPromptTypeConversion:
|
|||
result_from_list_string = await prompt.render(
|
||||
arguments={"numbers": "[1, 2, 3, 4, 5]"}
|
||||
)
|
||||
assert result_from_list_string == result_from_string
|
||||
assert result_from_list_string.messages == result_from_string.messages
|
||||
|
||||
async def test_various_type_conversions(self):
|
||||
"""Test type conversion for various data types."""
|
||||
|
|
@ -298,7 +310,7 @@ class TestPromptTypeConversion:
|
|||
expected_text = (
|
||||
"Alice (25): 3 scores, active=True, metadata keys=['project', 'version']"
|
||||
)
|
||||
assert result == [
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text=expected_text)
|
||||
)
|
||||
|
|
@ -329,7 +341,7 @@ class TestPromptTypeConversion:
|
|||
|
||||
# This should work with JSON parsing (integer as string)
|
||||
result1 = await prompt.render(arguments={"value": "42"})
|
||||
assert result1 == [
|
||||
assert result1.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text="Value: 42")
|
||||
)
|
||||
|
|
@ -337,7 +349,7 @@ class TestPromptTypeConversion:
|
|||
|
||||
# This should work with direct validation (already an integer string)
|
||||
result2 = await prompt.render(arguments={"value": "123"})
|
||||
assert result2 == [
|
||||
assert result2.messages == [
|
||||
PromptMessage(
|
||||
role="user", content=TextContent(type="text", text="Value: 123")
|
||||
)
|
||||
|
|
@ -358,7 +370,7 @@ class TestPromptTypeConversion:
|
|||
}
|
||||
)
|
||||
|
||||
assert result == [
|
||||
assert result.messages == [
|
||||
PromptMessage(
|
||||
role="user",
|
||||
content=TextContent(type="text", text="Hello world (repeated 3 times)"),
|
||||
|
|
|
|||
|
|
@ -400,10 +400,10 @@ class TestContextHandling:
|
|||
context = Context(fastmcp=mcp)
|
||||
|
||||
async with context:
|
||||
messages = await prompt.render(arguments={"x": 42})
|
||||
result = await prompt.render(arguments={"x": 42})
|
||||
|
||||
assert len(messages) == 1
|
||||
assert messages[0].content.text == "42" # type: ignore[attr-defined]
|
||||
assert len(result.messages) == 1
|
||||
assert result.messages[0].content.text == "42" # type: ignore[attr-defined]
|
||||
|
||||
async def test_context_optional(self):
|
||||
"""Test that context is optional when rendering prompts."""
|
||||
|
|
@ -420,12 +420,12 @@ class TestContextHandling:
|
|||
context = Context(fastmcp=mcp)
|
||||
|
||||
async with context:
|
||||
messages = await prompt.render(
|
||||
result = await prompt.render(
|
||||
arguments={"x": 42},
|
||||
)
|
||||
|
||||
assert len(messages) == 1
|
||||
assert messages[0].content.text == "42" # type: ignore[attr-defined]
|
||||
assert len(result.messages) == 1
|
||||
assert result.messages[0].content.text == "42" # type: ignore[attr-defined]
|
||||
|
||||
async def test_annotated_context_parameter_detection(self):
|
||||
"""Test that annotated context parameters are properly detected in
|
||||
|
|
@ -460,5 +460,5 @@ class TestContextHandling:
|
|||
context = Context(fastmcp=mcp)
|
||||
|
||||
async with context:
|
||||
messages = await prompt.render(arguments={"topic": "cats"})
|
||||
assert messages[0].content.text == "Write about cats" # type: ignore[attr-defined]
|
||||
result = await prompt.render(arguments={"topic": "cats"})
|
||||
assert result.messages[0].content.text == "Write about cats" # type: ignore[attr-defined]
|
||||
|
|
|
|||
|
|
@ -848,7 +848,7 @@ class TestPromptDecorator:
|
|||
assert prompt.name == "fn"
|
||||
# Don't compare functions directly since validate_call wraps them
|
||||
content = await prompt.render()
|
||||
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
assert content.messages[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
|
||||
async def test_prompt_decorator_without_parentheses(self):
|
||||
mcp = FastMCP()
|
||||
|
|
@ -880,7 +880,7 @@ class TestPromptDecorator:
|
|||
prompt = prompts_dict["custom_name"]
|
||||
assert prompt.name == "custom_name"
|
||||
content = await prompt.render()
|
||||
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
assert content.messages[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
|
||||
async def test_prompt_decorator_with_description(self):
|
||||
mcp = FastMCP()
|
||||
|
|
@ -894,7 +894,7 @@ class TestPromptDecorator:
|
|||
prompt = prompts_dict["fn"]
|
||||
assert prompt.description == "A custom description"
|
||||
content = await prompt.render()
|
||||
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
assert content.messages[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
|
||||
async def test_prompt_decorator_with_parameters(self):
|
||||
mcp = FastMCP()
|
||||
|
|
|
|||
|
|
@ -2194,7 +2194,7 @@ class TestPrompts:
|
|||
assert prompt.name == "fn"
|
||||
# Don't compare functions directly since validate_call wraps them
|
||||
content = await prompt.render()
|
||||
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
assert content.messages[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
|
||||
async def test_prompt_decorator_with_name(self):
|
||||
"""Test prompt decorator with custom name."""
|
||||
|
|
@ -2209,7 +2209,7 @@ class TestPrompts:
|
|||
prompt = prompts_dict["custom_name"]
|
||||
assert prompt.name == "custom_name"
|
||||
content = await prompt.render()
|
||||
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
assert content.messages[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
|
||||
async def test_prompt_decorator_with_description(self):
|
||||
"""Test prompt decorator with custom description."""
|
||||
|
|
@ -2224,7 +2224,7 @@ class TestPrompts:
|
|||
prompt = prompts_dict["fn"]
|
||||
assert prompt.description == "A custom description"
|
||||
content = await prompt.render()
|
||||
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
assert content.messages[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
||||
|
||||
async def test_prompt_decorator_with_parens(self):
|
||||
mcp = FastMCP()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue