mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-31 11:33:20 +02:00
Addresses #541: - Server now auto-deserializes JSON string args (list, dict, BaseModel) for prompts. This simplifies server-side prompt logic by reducing boilerplate `json.loads()` calls. - Docs updated to clarify `list_resource_templates` usage for templatized resources. - Docs updated to require client-side `json.dumps()` for complex `get_prompt` arguments, resolving the original Pydantic error. - Adds a new example (`examples/dynamic_story_prompt/`) demonstrating the server-side deserialization benefit and correct client-side serialization. Closes #541. --- Notes for Reviewers: - **Server-Side Auto-Deserialization:** This change introduces a "magic" `json.loads()` in `Prompt.render`. This is an intentional DX improvement. It only triggers for `str` inputs targeting `list`, `dict`, or `BaseModel` type hints. If `json.loads()` fails (e.g., malformed JSON), the original string is passed to Pydantic's `validate_call`, ensuring robust error handling. This avoids boilerplate in user prompt functions. - **Client `get_prompt()` Return Value:** The `examples/dynamic_story_prompt/story_client.py` parses the result of `client.get_prompt()` by iterating and looking for a `('messages', ...)` tuple. This reflects the observed behavior of the current `client.get_prompt()`. This commit does *not* change `client.get_prompt()`'s return behavior; the example merely adapts to it. A separate discussion might be warranted for potentially simplifying `client.get_prompt()`'s return signature in the future.
324 lines
No EOL
13 KiB
Text
324 lines
No EOL
13 KiB
Text
---
|
||
title: Prompts
|
||
sidebarTitle: Prompts
|
||
description: Create reusable, parameterized prompt templates for MCP clients.
|
||
icon: message-lines
|
||
---
|
||
|
||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||
|
||
Prompts are reusable message templates that help LLMs generate structured, purposeful responses. FastMCP simplifies defining these templates, primarily using the `@mcp.prompt` decorator.
|
||
|
||
## What Are Prompts?
|
||
|
||
Prompts provide parameterized message templates for LLMs. When a client requests a prompt:
|
||
|
||
1. FastMCP finds the corresponding prompt definition.
|
||
2. If it has parameters, they are validated against your function signature.
|
||
3. Your function executes with the validated inputs.
|
||
4. The generated message(s) are returned to the LLM to guide its response.
|
||
|
||
This allows you to define consistent, reusable templates that LLMs can use across different clients and contexts.
|
||
|
||
## Prompts
|
||
|
||
### The `@prompt` Decorator
|
||
|
||
The most common way to define a prompt is by decorating a Python function. The decorator uses the function name as the prompt's identifier.
|
||
|
||
```python
|
||
from fastmcp import FastMCP
|
||
from fastmcp.prompts.prompt import Message, PromptMessage, TextContent
|
||
|
||
mcp = FastMCP(name="PromptServer")
|
||
|
||
# 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."""
|
||
return f"Can you please explain the concept of '{topic}'?"
|
||
|
||
# Prompt returning a specific message type
|
||
@mcp.prompt()
|
||
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 PromptMessage(role="user", content=TextContent(type="text", text=content))
|
||
```
|
||
|
||
**Key Concepts:**
|
||
|
||
* **Name:** By default, the prompt name is taken from the function name.
|
||
* **Parameters:** The function parameters define the inputs needed to generate the prompt.
|
||
* **Inferred Metadata:** By default:
|
||
* Prompt Name: Taken from the function name (`ask_about_topic`).
|
||
* Prompt Description: Taken from the function's docstring.
|
||
<Tip>
|
||
Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
|
||
</Tip>
|
||
|
||
### Return Values
|
||
|
||
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).
|
||
- **`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 [
|
||
Message(f"Let's roleplay. You are {character}. The situation is: {situation}"),
|
||
Message("Okay, I understand. I am ready. What happens next?", role="assistant")
|
||
]
|
||
```
|
||
|
||
### Type Annotations
|
||
|
||
Type annotations are important for prompts. They:
|
||
1. Inform FastMCP about the expected types for each parameter.
|
||
2. Allow validation of parameters received from clients.
|
||
3. Are used to generate the prompt's schema for the MCP protocol.
|
||
|
||
```python
|
||
from pydantic import Field
|
||
from typing import Literal, Optional
|
||
|
||
@mcp.prompt()
|
||
def generate_content_request(
|
||
topic: str = Field(description="The main subject to cover"),
|
||
format: Literal["blog", "email", "social"] = "blog",
|
||
tone: str = "professional",
|
||
word_count: Optional[int] = None
|
||
) -> str:
|
||
"""Create a request for generating content in a specific format."""
|
||
prompt = f"Please write a {format} post about {topic} in a {tone} tone."
|
||
|
||
if word_count:
|
||
prompt += f" It should be approximately {word_count} words long."
|
||
|
||
return prompt
|
||
```
|
||
|
||
|
||
### Required vs. Optional Parameters
|
||
|
||
Parameters in your function signature are considered **required** unless they have a default value.
|
||
|
||
```python
|
||
@mcp.prompt()
|
||
def data_analysis_prompt(
|
||
data_uri: str, # Required - no default value
|
||
analysis_type: str = "summary", # Optional - has default value
|
||
include_charts: bool = False # Optional - has default value
|
||
) -> str:
|
||
"""Creates a request to analyze data with specific parameters."""
|
||
prompt = f"Please perform a '{analysis_type}' analysis on the data found at {data_uri}."
|
||
if include_charts:
|
||
prompt += " Include relevant charts and visualizations."
|
||
return prompt
|
||
```
|
||
|
||
In this example, the client *must* provide `data_uri`. If `analysis_type` or `include_charts` are omitted, their default values will be used.
|
||
|
||
### Prompt Metadata
|
||
|
||
While FastMCP infers the name and description from your function, you can override these and add tags using arguments to the `@mcp.prompt` decorator:
|
||
|
||
```python
|
||
@mcp.prompt(
|
||
name="analyze_data_request", # Custom prompt name
|
||
description="Creates a request to analyze data with specific parameters", # Custom description
|
||
tags={"analysis", "data"} # Optional categorization tags
|
||
)
|
||
def data_analysis_prompt(
|
||
data_uri: str = Field(description="The URI of the resource containing the data."),
|
||
analysis_type: str = Field(default="summary", description="Type of analysis.")
|
||
) -> str:
|
||
"""This docstring is ignored when description is provided."""
|
||
return f"Please perform a '{analysis_type}' analysis on the data found at {data_uri}."
|
||
```
|
||
|
||
- **`name`**: Sets the explicit prompt name exposed via MCP.
|
||
- **`description`**: Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose.
|
||
- **`tags`**: A set of strings used to categorize the prompt. Clients *might* use tags to filter or group available prompts.
|
||
|
||
### Asynchronous Prompts
|
||
|
||
FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) functions as prompts.
|
||
|
||
```python
|
||
# Synchronous prompt
|
||
@mcp.prompt()
|
||
def simple_question(question: str) -> str:
|
||
"""Generates a simple question to ask the LLM."""
|
||
return f"Question: {question}"
|
||
|
||
# Asynchronous prompt
|
||
@mcp.prompt()
|
||
async def data_based_prompt(data_id: str) -> str:
|
||
"""Generates a prompt based on data that needs to be fetched."""
|
||
# In a real scenario, you might fetch data from a database or API
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get(f"https://api.example.com/data/{data_id}") as response:
|
||
data = await response.json()
|
||
return f"Analyze this data: {data['content']}"
|
||
```
|
||
|
||
Use `async def` when your prompt function performs I/O operations like network requests, database queries, file I/O, or external service calls.
|
||
|
||
### Accessing MCP Context
|
||
|
||
<VersionBadge version="2.2.5" />
|
||
|
||
Prompts can access additional MCP information and features through the `Context` object. To access it, add a parameter to your prompt function with a type annotation of `Context`:
|
||
|
||
```python {6}
|
||
from fastmcp import FastMCP, Context
|
||
|
||
mcp = FastMCP(name="PromptServer")
|
||
|
||
@mcp.prompt()
|
||
async def generate_report_request(report_type: str, ctx: Context) -> str:
|
||
"""Generates a request for a report."""
|
||
return f"Please create a {report_type} report. Request ID: {ctx.request_id}"
|
||
```
|
||
|
||
For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
|
||
|
||
## Server Behavior
|
||
|
||
### Duplicate Prompts
|
||
|
||
<VersionBadge version="2.1.0" />
|
||
|
||
You can configure how the FastMCP server handles attempts to register multiple prompts with the same name. Use the `on_duplicate_prompts` setting during `FastMCP` initialization.
|
||
|
||
```python
|
||
from fastmcp import FastMCP
|
||
|
||
mcp = FastMCP(
|
||
name="PromptServer",
|
||
on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated
|
||
)
|
||
|
||
@mcp.prompt()
|
||
def greeting(): return "Hello, how can I help you today?"
|
||
|
||
# This registration attempt will raise a ValueError because
|
||
# "greeting" is already registered and the behavior is "error".
|
||
# @mcp.prompt()
|
||
# def greeting(): return "Hi there! What can I do for you?"
|
||
```
|
||
|
||
The duplicate behavior options are:
|
||
|
||
- `"warn"` (default): Logs a warning, and the new prompt replaces the old one.
|
||
- `"error"`: Raises a `ValueError`, preventing the duplicate registration.
|
||
- `"replace"`: Silently replaces the existing prompt with the new one.
|
||
- `"ignore"`: Keeps the original prompt and ignores the new registration attempt.
|
||
|
||
### Automatic Deserialization of String Arguments
|
||
|
||
<VersionBadge version="2.4.1" />
|
||
|
||
FastMCP enhances developer experience by automatically handling the deserialization of string arguments for your prompt functions when they are intended to be complex Python types like lists, dictionaries, or Pydantic models.
|
||
|
||
When you define your prompt functions, you can use Pythonic type hints to specify the data structures you want to work with directly:
|
||
|
||
```python server.py
|
||
from fastmcp import FastMCP
|
||
from pydantic import BaseModel
|
||
|
||
mcp = FastMCP(name="PromptDemoServer")
|
||
|
||
class MyConfig(BaseModel):
|
||
param_a: str
|
||
param_b: int
|
||
is_active: bool = True
|
||
|
||
@mcp.prompt()
|
||
def process_user_data(
|
||
user_id: int,
|
||
preferences: list[str],
|
||
settings: MyConfig,
|
||
metadata: dict[str, str | int]
|
||
) -> str:
|
||
"""
|
||
Generates a prompt based on processed user data, preferences,
|
||
settings, and metadata.
|
||
"""
|
||
# Inside this function, you directly work with Python objects:
|
||
# - user_id is an int
|
||
# - preferences is a list of strings
|
||
# - settings is an instance of MyConfig
|
||
# - metadata is a dictionary
|
||
|
||
pref_string = ", ".join(preferences)
|
||
return (
|
||
f"User {user_id} preferences: {pref_string}. "
|
||
f"Settings: {settings.param_a} (Active: {settings.is_active}, Value: {settings.param_b}). "
|
||
f"Metadata count: {len(metadata)}."
|
||
)
|
||
```
|
||
|
||
**How it Works:**
|
||
|
||
If a client sends arguments for `preferences`, `settings`, or `metadata` as JSON strings (which is necessary for complex types due to the underlying MCP specification that expects `dict[str, str]` for prompt arguments – see [Client documentation](/clients/client#prompt-operations)), FastMCP's server-side logic will:
|
||
|
||
1. Detect that the incoming argument is a string.
|
||
2. Check your function's type hint for that parameter (e.g., `list[str]`, `MyConfig`, `dict[str, str | int]`).
|
||
3. If the type hint is `list`, `dict`, or a Pydantic `BaseModel` (or a generic version like `list[Any]`), FastMCP attempts to parse the string using `json.loads()`.
|
||
4. **On Success:** Your function receives the deserialized Python object (a `list`, `dict`, or `MyConfig` instance).
|
||
5. **On `json.JSONDecodeError`:** If the string is not valid JSON, it's passed to your function as-is. Pydantic's standard validation will then take over, likely raising a `ValidationError` if the string doesn't match the expected type (e.g., a string like `"not-a-list"` for a `list` parameter).
|
||
|
||
This behavior significantly reduces boilerplate in your prompt functions, as you don't need to manually call `json.loads()` and handle potential errors for every complex parameter.
|
||
|
||
<details>
|
||
<summary>Hypothetical Client Example</summary>
|
||
|
||
```python client.py
|
||
import asyncio
|
||
import json
|
||
from fastmcp import Client
|
||
|
||
async def run_client():
|
||
client = Client("server.py") # Or appropriate transport to your server
|
||
|
||
async with client:
|
||
user_prefs = ["notifications", "dark_mode"]
|
||
user_settings = {"param_a": "profile_v2", "param_b": 42, "is_active": False}
|
||
user_metadata = {"login_count": 10, "last_ip": "192.168.1.100"}
|
||
|
||
client_args = {
|
||
"user_id": 123, # Simple types are passed directly
|
||
"preferences": json.dumps(user_prefs),
|
||
"settings": json.dumps(user_settings),
|
||
"metadata": json.dumps(user_metadata)
|
||
}
|
||
|
||
print(f"Sending to server: {client_args}")
|
||
|
||
try:
|
||
prompt_messages = await client.get_prompt("process_user_data", client_args)
|
||
for msg in prompt_messages:
|
||
print(f"Server responded with role '{msg.role}': {msg.content.text}")
|
||
except Exception as e:
|
||
print(f"An error occurred: {e}")
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(run_client())
|
||
```
|
||
**Expected Client Output (if server responds directly with the generated string):**
|
||
```
|
||
# Sending to server: {'user_id': 123, 'preferences': '["notifications", "dark_mode"]', 'settings': '{"param_a": "profile_v2", "param_b": 42, "is_active": false}', 'metadata': '{"login_count": 10, "last_ip": "192.168.1.100"}'}
|
||
# Server responded with role 'user': User 123 preferences: notifications, dark_mode. Settings: profile_v2 (Active: False, Value: 42). Metadata count: 2.
|
||
```
|
||
</details>
|
||
|
||
## Advanced Usage |