Spaces:
Running
Running
Merge pull request #711 from jlowin/prompt-decorator
Browse files- README.md +2 -2
- docs/servers/context.mdx +1 -1
- docs/servers/fastmcp.mdx +1 -1
- docs/servers/prompts.mdx +10 -10
- src/fastmcp/server/server.py +61 -17
- tests/client/test_client.py +1 -1
- tests/client/test_sse.py +1 -1
- tests/client/test_streamable_http.py +1 -1
- tests/deprecated/test_deprecated.py +1 -1
- tests/deprecated/test_mount_separators.py +1 -1
- tests/server/http/test_http_dependencies.py +1 -1
- tests/server/test_import_server.py +4 -4
- tests/server/test_mount.py +4 -4
- tests/server/test_proxy.py +1 -1
- tests/server/test_run_server.py +1 -1
- tests/server/test_server.py +75 -11
- tests/server/test_server_interactions.py +15 -12
- tests/test_servers/fastmcp_server.py +1 -1
README.md
CHANGED
|
@@ -173,10 +173,10 @@ Learn more in the [**Resources & Templates Documentation**](https://gofastmcp.co
|
|
| 173 |
|
| 174 |
### Prompts
|
| 175 |
|
| 176 |
-
Prompts define reusable message templates to guide LLM interactions. Decorate functions with `@mcp.prompt
|
| 177 |
|
| 178 |
```python
|
| 179 |
-
@mcp.prompt
|
| 180 |
def summarize_request(text: str) -> str:
|
| 181 |
"""Generate a prompt asking for a summary."""
|
| 182 |
return f"Please summarize the following text:\n\n{text}"
|
|
|
|
| 173 |
|
| 174 |
### Prompts
|
| 175 |
|
| 176 |
+
Prompts define reusable message templates to guide LLM interactions. Decorate functions with `@mcp.prompt`. Return strings or `Message` objects.
|
| 177 |
|
| 178 |
```python
|
| 179 |
+
@mcp.prompt
|
| 180 |
def summarize_request(text: str) -> str:
|
| 181 |
"""Generate a prompt asking for a summary."""
|
| 182 |
return f"Please summarize the following text:\n\n{text}"
|
docs/servers/context.mdx
CHANGED
|
@@ -71,7 +71,7 @@ async def get_user_profile(user_id: str, ctx: Context) -> dict:
|
|
| 71 |
<VersionBadge version="2.2.5" />
|
| 72 |
|
| 73 |
```python
|
| 74 |
-
@mcp.prompt
|
| 75 |
async def data_analysis_request(dataset: str, ctx: Context) -> str:
|
| 76 |
"""Generate a request to analyze data with contextual information."""
|
| 77 |
# Context is available as the ctx parameter
|
|
|
|
| 71 |
<VersionBadge version="2.2.5" />
|
| 72 |
|
| 73 |
```python
|
| 74 |
+
@mcp.prompt
|
| 75 |
async def data_analysis_request(dataset: str, ctx: Context) -> str:
|
| 76 |
"""Generate a request to analyze data with contextual information."""
|
| 77 |
# Context is available as the ctx parameter
|
docs/servers/fastmcp.mdx
CHANGED
|
@@ -87,7 +87,7 @@ See [Resources & Templates](/servers/resources) for detailed documentation.
|
|
| 87 |
Prompts are reusable message templates for guiding the LLM.
|
| 88 |
|
| 89 |
```python
|
| 90 |
-
@mcp.prompt
|
| 91 |
def analyze_data(data_points: list[float]) -> str:
|
| 92 |
"""Creates a prompt asking for analysis of numerical data."""
|
| 93 |
formatted_data = ", ".join(str(point) for point in data_points)
|
|
|
|
| 87 |
Prompts are reusable message templates for guiding the LLM.
|
| 88 |
|
| 89 |
```python
|
| 90 |
+
@mcp.prompt
|
| 91 |
def analyze_data(data_points: list[float]) -> str:
|
| 92 |
"""Creates a prompt asking for analysis of numerical data."""
|
| 93 |
formatted_data = ", ".join(str(point) for point in data_points)
|
docs/servers/prompts.mdx
CHANGED
|
@@ -33,13 +33,13 @@ from fastmcp.prompts.prompt import Message, PromptMessage, TextContent
|
|
| 33 |
mcp = FastMCP(name="PromptServer")
|
| 34 |
|
| 35 |
# Basic prompt returning a string (converted to user message automatically)
|
| 36 |
-
@mcp.prompt
|
| 37 |
def ask_about_topic(topic: str) -> str:
|
| 38 |
"""Generates a user message asking for an explanation of a topic."""
|
| 39 |
return f"Can you please explain the concept of '{topic}'?"
|
| 40 |
|
| 41 |
# Prompt returning a specific message type
|
| 42 |
-
@mcp.prompt
|
| 43 |
def generate_code_request(language: str, task_description: str) -> PromptMessage:
|
| 44 |
"""Generates a user message requesting code generation."""
|
| 45 |
content = f"Write a {language} function that performs the following task: {task_description}"
|
|
@@ -69,7 +69,7 @@ FastMCP intelligently handles different return types from your prompt function:
|
|
| 69 |
```python
|
| 70 |
from fastmcp.prompts.prompt import Message
|
| 71 |
|
| 72 |
-
@mcp.prompt
|
| 73 |
def roleplay_scenario(character: str, situation: str) -> list[Message]:
|
| 74 |
"""Sets up a roleplaying scenario with initial messages."""
|
| 75 |
return [
|
|
@@ -89,7 +89,7 @@ Type annotations are important for prompts. They:
|
|
| 89 |
from pydantic import Field
|
| 90 |
from typing import Literal, Optional
|
| 91 |
|
| 92 |
-
@mcp.prompt
|
| 93 |
def generate_content_request(
|
| 94 |
topic: str = Field(description="The main subject to cover"),
|
| 95 |
format: Literal["blog", "email", "social"] = "blog",
|
|
@@ -111,7 +111,7 @@ def generate_content_request(
|
|
| 111 |
Parameters in your function signature are considered **required** unless they have a default value.
|
| 112 |
|
| 113 |
```python
|
| 114 |
-
@mcp.prompt
|
| 115 |
def data_analysis_prompt(
|
| 116 |
data_uri: str, # Required - no default value
|
| 117 |
analysis_type: str = "summary", # Optional - has default value
|
|
@@ -154,13 +154,13 @@ FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`)
|
|
| 154 |
|
| 155 |
```python
|
| 156 |
# Synchronous prompt
|
| 157 |
-
@mcp.prompt
|
| 158 |
def simple_question(question: str) -> str:
|
| 159 |
"""Generates a simple question to ask the LLM."""
|
| 160 |
return f"Question: {question}"
|
| 161 |
|
| 162 |
# Asynchronous prompt
|
| 163 |
-
@mcp.prompt
|
| 164 |
async def data_based_prompt(data_id: str) -> str:
|
| 165 |
"""Generates a prompt based on data that needs to be fetched."""
|
| 166 |
# In a real scenario, you might fetch data from a database or API
|
|
@@ -183,7 +183,7 @@ from fastmcp import FastMCP, Context
|
|
| 183 |
|
| 184 |
mcp = FastMCP(name="PromptServer")
|
| 185 |
|
| 186 |
-
@mcp.prompt
|
| 187 |
async def generate_report_request(report_type: str, ctx: Context) -> str:
|
| 188 |
"""Generates a request for a report."""
|
| 189 |
return f"Please create a {report_type} report. Request ID: {ctx.request_id}"
|
|
@@ -207,12 +207,12 @@ mcp = FastMCP(
|
|
| 207 |
on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated
|
| 208 |
)
|
| 209 |
|
| 210 |
-
@mcp.prompt
|
| 211 |
def greeting(): return "Hello, how can I help you today?"
|
| 212 |
|
| 213 |
# This registration attempt will raise a ValueError because
|
| 214 |
# "greeting" is already registered and the behavior is "error".
|
| 215 |
-
# @mcp.prompt
|
| 216 |
# def greeting(): return "Hi there! What can I do for you?"
|
| 217 |
```
|
| 218 |
|
|
|
|
| 33 |
mcp = FastMCP(name="PromptServer")
|
| 34 |
|
| 35 |
# Basic prompt returning a string (converted to user message automatically)
|
| 36 |
+
@mcp.prompt
|
| 37 |
def ask_about_topic(topic: str) -> str:
|
| 38 |
"""Generates a user message asking for an explanation of a topic."""
|
| 39 |
return f"Can you please explain the concept of '{topic}'?"
|
| 40 |
|
| 41 |
# Prompt returning a specific message type
|
| 42 |
+
@mcp.prompt
|
| 43 |
def generate_code_request(language: str, task_description: str) -> PromptMessage:
|
| 44 |
"""Generates a user message requesting code generation."""
|
| 45 |
content = f"Write a {language} function that performs the following task: {task_description}"
|
|
|
|
| 69 |
```python
|
| 70 |
from fastmcp.prompts.prompt import Message
|
| 71 |
|
| 72 |
+
@mcp.prompt
|
| 73 |
def roleplay_scenario(character: str, situation: str) -> list[Message]:
|
| 74 |
"""Sets up a roleplaying scenario with initial messages."""
|
| 75 |
return [
|
|
|
|
| 89 |
from pydantic import Field
|
| 90 |
from typing import Literal, Optional
|
| 91 |
|
| 92 |
+
@mcp.prompt
|
| 93 |
def generate_content_request(
|
| 94 |
topic: str = Field(description="The main subject to cover"),
|
| 95 |
format: Literal["blog", "email", "social"] = "blog",
|
|
|
|
| 111 |
Parameters in your function signature are considered **required** unless they have a default value.
|
| 112 |
|
| 113 |
```python
|
| 114 |
+
@mcp.prompt
|
| 115 |
def data_analysis_prompt(
|
| 116 |
data_uri: str, # Required - no default value
|
| 117 |
analysis_type: str = "summary", # Optional - has default value
|
|
|
|
| 154 |
|
| 155 |
```python
|
| 156 |
# Synchronous prompt
|
| 157 |
+
@mcp.prompt
|
| 158 |
def simple_question(question: str) -> str:
|
| 159 |
"""Generates a simple question to ask the LLM."""
|
| 160 |
return f"Question: {question}"
|
| 161 |
|
| 162 |
# Asynchronous prompt
|
| 163 |
+
@mcp.prompt
|
| 164 |
async def data_based_prompt(data_id: str) -> str:
|
| 165 |
"""Generates a prompt based on data that needs to be fetched."""
|
| 166 |
# In a real scenario, you might fetch data from a database or API
|
|
|
|
| 183 |
|
| 184 |
mcp = FastMCP(name="PromptServer")
|
| 185 |
|
| 186 |
+
@mcp.prompt
|
| 187 |
async def generate_report_request(report_type: str, ctx: Context) -> str:
|
| 188 |
"""Generates a request for a report."""
|
| 189 |
return f"Please create a {report_type} report. Request ID: {ctx.request_id}"
|
|
|
|
| 207 |
on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated
|
| 208 |
)
|
| 209 |
|
| 210 |
+
@mcp.prompt
|
| 211 |
def greeting(): return "Hello, how can I help you today?"
|
| 212 |
|
| 213 |
# This registration attempt will raise a ValueError because
|
| 214 |
# "greeting" is already registered and the behavior is "error".
|
| 215 |
+
# @mcp.prompt
|
| 216 |
# def greeting(): return "Hi there! What can I do for you?"
|
| 217 |
```
|
| 218 |
|
src/fastmcp/server/server.py
CHANGED
|
@@ -782,23 +782,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 782 |
|
| 783 |
def prompt(
|
| 784 |
self,
|
|
|
|
|
|
|
| 785 |
name: str | None = None,
|
| 786 |
description: str | None = None,
|
| 787 |
tags: set[str] | None = None,
|
| 788 |
-
) -> Callable[[AnyFunction], AnyFunction]:
|
| 789 |
"""Decorator to register a prompt.
|
| 790 |
|
| 791 |
Prompts can optionally request a Context object by adding a parameter with the
|
| 792 |
Context type annotation. The context provides access to MCP capabilities like
|
| 793 |
logging, progress reporting, and session information.
|
| 794 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 795 |
Args:
|
| 796 |
-
|
| 797 |
description: Optional description of what the prompt does
|
| 798 |
tags: Optional set of tags for categorizing the prompt
|
|
|
|
| 799 |
|
| 800 |
Example:
|
| 801 |
-
@server.prompt
|
| 802 |
def analyze_table(table_name: str) -> list[Message]:
|
| 803 |
schema = read_table_schema(table_name)
|
| 804 |
return [
|
|
@@ -808,7 +818,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 808 |
}
|
| 809 |
]
|
| 810 |
|
| 811 |
-
@server.prompt
|
| 812 |
def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
|
| 813 |
ctx.info(f"Analyzing table {table_name}")
|
| 814 |
schema = read_table_schema(table_name)
|
|
@@ -819,8 +829,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 819 |
}
|
| 820 |
]
|
| 821 |
|
| 822 |
-
@server.prompt()
|
| 823 |
-
|
| 824 |
content = await read_file(path)
|
| 825 |
return [
|
| 826 |
{
|
|
@@ -834,26 +844,60 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 834 |
}
|
| 835 |
}
|
| 836 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
"""
|
| 838 |
-
#
|
| 839 |
-
if callable(
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
|
| 845 |
-
|
| 846 |
prompt = Prompt.from_function(
|
| 847 |
fn=fn,
|
| 848 |
-
name=
|
| 849 |
description=description,
|
| 850 |
tags=tags,
|
| 851 |
)
|
| 852 |
-
|
| 853 |
self.add_prompt(prompt)
|
| 854 |
-
return DecoratedFunction(fn)
|
| 855 |
|
| 856 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 857 |
|
| 858 |
async def run_stdio_async(self) -> None:
|
| 859 |
"""Run the server using stdio transport."""
|
|
|
|
| 782 |
|
| 783 |
def prompt(
|
| 784 |
self,
|
| 785 |
+
name_or_fn: str | AnyFunction | None = None,
|
| 786 |
+
*,
|
| 787 |
name: str | None = None,
|
| 788 |
description: str | None = None,
|
| 789 |
tags: set[str] | None = None,
|
| 790 |
+
) -> Callable[[AnyFunction], AnyFunction] | AnyFunction:
|
| 791 |
"""Decorator to register a prompt.
|
| 792 |
|
| 793 |
Prompts can optionally request a Context object by adding a parameter with the
|
| 794 |
Context type annotation. The context provides access to MCP capabilities like
|
| 795 |
logging, progress reporting, and session information.
|
| 796 |
|
| 797 |
+
This decorator supports multiple calling patterns:
|
| 798 |
+
- @server.prompt (without parentheses)
|
| 799 |
+
- @server.prompt (with empty parentheses)
|
| 800 |
+
- @server.prompt("custom_name") (with name as first argument)
|
| 801 |
+
- @server.prompt(name="custom_name") (with name as keyword argument)
|
| 802 |
+
- server.prompt(function, name="custom_name") (direct function call)
|
| 803 |
+
|
| 804 |
Args:
|
| 805 |
+
name_or_fn: Either a function (when used as @prompt), a string name, or None
|
| 806 |
description: Optional description of what the prompt does
|
| 807 |
tags: Optional set of tags for categorizing the prompt
|
| 808 |
+
name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
|
| 809 |
|
| 810 |
Example:
|
| 811 |
+
@server.prompt
|
| 812 |
def analyze_table(table_name: str) -> list[Message]:
|
| 813 |
schema = read_table_schema(table_name)
|
| 814 |
return [
|
|
|
|
| 818 |
}
|
| 819 |
]
|
| 820 |
|
| 821 |
+
@server.prompt
|
| 822 |
def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
|
| 823 |
ctx.info(f"Analyzing table {table_name}")
|
| 824 |
schema = read_table_schema(table_name)
|
|
|
|
| 829 |
}
|
| 830 |
]
|
| 831 |
|
| 832 |
+
@server.prompt("custom_name")
|
| 833 |
+
def analyze_file(path: str) -> list[Message]:
|
| 834 |
content = await read_file(path)
|
| 835 |
return [
|
| 836 |
{
|
|
|
|
| 844 |
}
|
| 845 |
}
|
| 846 |
]
|
| 847 |
+
|
| 848 |
+
@server.prompt(name="custom_name")
|
| 849 |
+
def another_prompt(data: str) -> list[Message]:
|
| 850 |
+
return [{"role": "user", "content": data}]
|
| 851 |
+
|
| 852 |
+
# Direct function call
|
| 853 |
+
server.prompt(my_function, name="custom_name")
|
| 854 |
"""
|
| 855 |
+
# Determine the actual name and function based on the calling pattern
|
| 856 |
+
if callable(name_or_fn):
|
| 857 |
+
# Case 1: @prompt (without parens) - function passed directly as decorator
|
| 858 |
+
# Case 2: direct call like prompt(fn, name="something")
|
| 859 |
+
fn = name_or_fn
|
| 860 |
+
prompt_name = name # Use keyword name if provided, otherwise None
|
| 861 |
|
| 862 |
+
# Register the prompt immediately
|
| 863 |
prompt = Prompt.from_function(
|
| 864 |
fn=fn,
|
| 865 |
+
name=prompt_name,
|
| 866 |
description=description,
|
| 867 |
tags=tags,
|
| 868 |
)
|
|
|
|
| 869 |
self.add_prompt(prompt)
|
|
|
|
| 870 |
|
| 871 |
+
# If name is provided, this is a direct call, return original function for consistency with tools
|
| 872 |
+
# If name is None, this is @prompt without parens, return DecoratedFunction for proper method handling
|
| 873 |
+
if name is not None:
|
| 874 |
+
return fn # Direct function call
|
| 875 |
+
else:
|
| 876 |
+
return DecoratedFunction(fn) # Decorator usage
|
| 877 |
+
|
| 878 |
+
elif isinstance(name_or_fn, str):
|
| 879 |
+
# Case 3: @prompt("custom_name") - name passed as first argument
|
| 880 |
+
if name is not None:
|
| 881 |
+
raise TypeError(
|
| 882 |
+
"Cannot specify both a name as first argument and as keyword argument. "
|
| 883 |
+
f"Use either @prompt('{name_or_fn}') or @prompt(name='{name}'), not both."
|
| 884 |
+
)
|
| 885 |
+
prompt_name = name_or_fn
|
| 886 |
+
elif name_or_fn is None:
|
| 887 |
+
# Case 4: @prompt() or @prompt(name="something") - use keyword name
|
| 888 |
+
prompt_name = name
|
| 889 |
+
else:
|
| 890 |
+
raise TypeError(
|
| 891 |
+
f"First argument to @prompt must be a function, string, or None, got {type(name_or_fn)}"
|
| 892 |
+
)
|
| 893 |
+
|
| 894 |
+
# Return partial for cases where we need to wait for the function
|
| 895 |
+
return partial(
|
| 896 |
+
self.prompt,
|
| 897 |
+
name=prompt_name,
|
| 898 |
+
description=description,
|
| 899 |
+
tags=tags,
|
| 900 |
+
)
|
| 901 |
|
| 902 |
async def run_stdio_async(self) -> None:
|
| 903 |
"""Run the server using stdio transport."""
|
tests/client/test_client.py
CHANGED
|
@@ -53,7 +53,7 @@ def fastmcp_server():
|
|
| 53 |
return {"id": user_id, "name": f"User {user_id}", "active": True}
|
| 54 |
|
| 55 |
# Add a prompt
|
| 56 |
-
@server.prompt
|
| 57 |
def welcome(name: str) -> str:
|
| 58 |
"""Example greeting prompt."""
|
| 59 |
return f"Welcome to FastMCP, {name}!"
|
|
|
|
| 53 |
return {"id": user_id, "name": f"User {user_id}", "active": True}
|
| 54 |
|
| 55 |
# Add a prompt
|
| 56 |
+
@server.prompt
|
| 57 |
def welcome(name: str) -> str:
|
| 58 |
"""Example greeting prompt."""
|
| 59 |
return f"Welcome to FastMCP, {name}!"
|
tests/client/test_sse.py
CHANGED
|
@@ -55,7 +55,7 @@ def fastmcp_server():
|
|
| 55 |
return dict(request.headers)
|
| 56 |
|
| 57 |
# Add a prompt
|
| 58 |
-
@server.prompt
|
| 59 |
def welcome(name: str) -> str:
|
| 60 |
"""Example greeting prompt."""
|
| 61 |
return f"Welcome to FastMCP, {name}!"
|
|
|
|
| 55 |
return dict(request.headers)
|
| 56 |
|
| 57 |
# Add a prompt
|
| 58 |
+
@server.prompt
|
| 59 |
def welcome(name: str) -> str:
|
| 60 |
"""Example greeting prompt."""
|
| 61 |
return f"Welcome to FastMCP, {name}!"
|
tests/client/test_streamable_http.py
CHANGED
|
@@ -55,7 +55,7 @@ def fastmcp_server():
|
|
| 55 |
return dict(request.headers)
|
| 56 |
|
| 57 |
# Add a prompt
|
| 58 |
-
@server.prompt
|
| 59 |
def welcome(name: str) -> str:
|
| 60 |
"""Example greeting prompt."""
|
| 61 |
return f"Welcome to FastMCP, {name}!"
|
|
|
|
| 55 |
return dict(request.headers)
|
| 56 |
|
| 57 |
# Add a prompt
|
| 58 |
+
@server.prompt
|
| 59 |
def welcome(name: str) -> str:
|
| 60 |
"""Example greeting prompt."""
|
| 61 |
return f"Welcome to FastMCP, {name}!"
|
tests/deprecated/test_deprecated.py
CHANGED
|
@@ -133,7 +133,7 @@ def test_mount_prompt_separator_deprecation_warning():
|
|
| 133 |
main_app.mount("sub", sub_app, prompt_separator="-")
|
| 134 |
|
| 135 |
# Verify the separator is ignored and the default is used
|
| 136 |
-
@sub_app.prompt
|
| 137 |
def test_prompt():
|
| 138 |
return "test"
|
| 139 |
|
|
|
|
| 133 |
main_app.mount("sub", sub_app, prompt_separator="-")
|
| 134 |
|
| 135 |
# Verify the separator is ignored and the default is used
|
| 136 |
+
@sub_app.prompt
|
| 137 |
def test_prompt():
|
| 138 |
return "test"
|
| 139 |
|
tests/deprecated/test_mount_separators.py
CHANGED
|
@@ -53,7 +53,7 @@ def test_mount_prompt_separator_deprecation_warning():
|
|
| 53 |
main_app.mount("sub", sub_app, prompt_separator="-")
|
| 54 |
|
| 55 |
# Verify the separator is ignored and the default is used
|
| 56 |
-
@sub_app.prompt
|
| 57 |
def test_prompt():
|
| 58 |
return "test"
|
| 59 |
|
|
|
|
| 53 |
main_app.mount("sub", sub_app, prompt_separator="-")
|
| 54 |
|
| 55 |
# Verify the separator is ignored and the default is used
|
| 56 |
+
@sub_app.prompt
|
| 57 |
def test_prompt():
|
| 58 |
return "test"
|
| 59 |
|
tests/server/http/test_http_dependencies.py
CHANGED
|
@@ -28,7 +28,7 @@ def fastmcp_server():
|
|
| 28 |
return dict(request.headers)
|
| 29 |
|
| 30 |
# Add a prompt
|
| 31 |
-
@server.prompt
|
| 32 |
def get_headers_prompt() -> str:
|
| 33 |
"""Get the HTTP headers from the request."""
|
| 34 |
request = get_http_request()
|
|
|
|
| 28 |
return dict(request.headers)
|
| 29 |
|
| 30 |
# Add a prompt
|
| 31 |
+
@server.prompt
|
| 32 |
def get_headers_prompt() -> str:
|
| 33 |
"""Get the HTTP headers from the request."""
|
| 34 |
request = get_http_request()
|
tests/server/test_import_server.py
CHANGED
|
@@ -130,7 +130,7 @@ async def test_import_with_prompts():
|
|
| 130 |
assistant_app = FastMCP("AssistantApp")
|
| 131 |
|
| 132 |
# Add a prompt to the assistant app
|
| 133 |
-
@assistant_app.prompt
|
| 134 |
def greeting(name: str) -> str:
|
| 135 |
return f"Hello, {name}!"
|
| 136 |
|
|
@@ -174,11 +174,11 @@ async def test_import_multiple_prompts():
|
|
| 174 |
sql_app = FastMCP("SQLApp")
|
| 175 |
|
| 176 |
# Add prompts to each app
|
| 177 |
-
@python_app.prompt
|
| 178 |
def review_python(code: str) -> str:
|
| 179 |
return f"Reviewing Python code:\n{code}"
|
| 180 |
|
| 181 |
-
@sql_app.prompt
|
| 182 |
def explain_sql(query: str) -> str:
|
| 183 |
return f"Explaining SQL query:\n{query}"
|
| 184 |
|
|
@@ -316,7 +316,7 @@ async def test_import_with_proxy_prompts():
|
|
| 316 |
main_app = FastMCP("MainApp")
|
| 317 |
api_app = FastMCP("APIApp")
|
| 318 |
|
| 319 |
-
@api_app.prompt
|
| 320 |
def greeting(name: str) -> str:
|
| 321 |
"""Example greeting prompt."""
|
| 322 |
return f"Hello, {name} from API!"
|
|
|
|
| 130 |
assistant_app = FastMCP("AssistantApp")
|
| 131 |
|
| 132 |
# Add a prompt to the assistant app
|
| 133 |
+
@assistant_app.prompt
|
| 134 |
def greeting(name: str) -> str:
|
| 135 |
return f"Hello, {name}!"
|
| 136 |
|
|
|
|
| 174 |
sql_app = FastMCP("SQLApp")
|
| 175 |
|
| 176 |
# Add prompts to each app
|
| 177 |
+
@python_app.prompt
|
| 178 |
def review_python(code: str) -> str:
|
| 179 |
return f"Reviewing Python code:\n{code}"
|
| 180 |
|
| 181 |
+
@sql_app.prompt
|
| 182 |
def explain_sql(query: str) -> str:
|
| 183 |
return f"Explaining SQL query:\n{query}"
|
| 184 |
|
|
|
|
| 316 |
main_app = FastMCP("MainApp")
|
| 317 |
api_app = FastMCP("APIApp")
|
| 318 |
|
| 319 |
+
@api_app.prompt
|
| 320 |
def greeting(name: str) -> str:
|
| 321 |
"""Example greeting prompt."""
|
| 322 |
return f"Hello, {name} from API!"
|
tests/server/test_mount.py
CHANGED
|
@@ -194,7 +194,7 @@ class TestMultipleServerMount:
|
|
| 194 |
def working_resource():
|
| 195 |
return "Working resource"
|
| 196 |
|
| 197 |
-
@working_app.prompt
|
| 198 |
def working_prompt() -> str:
|
| 199 |
return "Working prompt"
|
| 200 |
|
|
@@ -384,7 +384,7 @@ class TestPrompts:
|
|
| 384 |
main_app = FastMCP("MainApp")
|
| 385 |
assistant_app = FastMCP("AssistantApp")
|
| 386 |
|
| 387 |
-
@assistant_app.prompt
|
| 388 |
def greeting(name: str) -> str:
|
| 389 |
return f"Hello, {name}!"
|
| 390 |
|
|
@@ -409,7 +409,7 @@ class TestPrompts:
|
|
| 409 |
main_app.mount("assistant", assistant_app)
|
| 410 |
|
| 411 |
# Add a prompt after mounting
|
| 412 |
-
@assistant_app.prompt
|
| 413 |
def farewell(name: str) -> str:
|
| 414 |
return f"Goodbye, {name}!"
|
| 415 |
|
|
@@ -507,7 +507,7 @@ class TestProxyServer:
|
|
| 507 |
# Create original server
|
| 508 |
original_server = FastMCP("OriginalServer")
|
| 509 |
|
| 510 |
-
@original_server.prompt
|
| 511 |
def welcome(name: str) -> str:
|
| 512 |
return f"Welcome, {name}!"
|
| 513 |
|
|
|
|
| 194 |
def working_resource():
|
| 195 |
return "Working resource"
|
| 196 |
|
| 197 |
+
@working_app.prompt
|
| 198 |
def working_prompt() -> str:
|
| 199 |
return "Working prompt"
|
| 200 |
|
|
|
|
| 384 |
main_app = FastMCP("MainApp")
|
| 385 |
assistant_app = FastMCP("AssistantApp")
|
| 386 |
|
| 387 |
+
@assistant_app.prompt
|
| 388 |
def greeting(name: str) -> str:
|
| 389 |
return f"Hello, {name}!"
|
| 390 |
|
|
|
|
| 409 |
main_app.mount("assistant", assistant_app)
|
| 410 |
|
| 411 |
# Add a prompt after mounting
|
| 412 |
+
@assistant_app.prompt
|
| 413 |
def farewell(name: str) -> str:
|
| 414 |
return f"Goodbye, {name}!"
|
| 415 |
|
|
|
|
| 507 |
# Create original server
|
| 508 |
original_server = FastMCP("OriginalServer")
|
| 509 |
|
| 510 |
+
@original_server.prompt
|
| 511 |
def welcome(name: str) -> str:
|
| 512 |
return f"Welcome, {name}!"
|
| 513 |
|
tests/server/test_proxy.py
CHANGED
|
@@ -61,7 +61,7 @@ def fastmcp_server():
|
|
| 61 |
|
| 62 |
# --- Prompts ---
|
| 63 |
|
| 64 |
-
@server.prompt
|
| 65 |
def welcome(name: str) -> str:
|
| 66 |
return f"Welcome to FastMCP, {name}!"
|
| 67 |
|
|
|
|
| 61 |
|
| 62 |
# --- Prompts ---
|
| 63 |
|
| 64 |
+
@server.prompt
|
| 65 |
def welcome(name: str) -> str:
|
| 66 |
return f"Welcome to FastMCP, {name}!"
|
| 67 |
|
tests/server/test_run_server.py
CHANGED
|
@@ -53,7 +53,7 @@
|
|
| 53 |
|
| 54 |
# # --- Prompts ---
|
| 55 |
|
| 56 |
-
# @server.prompt
|
| 57 |
# def welcome(name: str) -> str:
|
| 58 |
# return f"Welcome to FastMCP, {name}!"
|
| 59 |
|
|
|
|
| 53 |
|
| 54 |
# # --- Prompts ---
|
| 55 |
|
| 56 |
+
# @server.prompt
|
| 57 |
# def welcome(name: str) -> str:
|
| 58 |
# return f"Welcome to FastMCP, {name}!"
|
| 59 |
|
tests/server/test_server.py
CHANGED
|
@@ -659,7 +659,7 @@ class TestPromptDecorator:
|
|
| 659 |
async def test_prompt_decorator(self):
|
| 660 |
mcp = FastMCP()
|
| 661 |
|
| 662 |
-
@mcp.prompt
|
| 663 |
def fn() -> str:
|
| 664 |
return "Hello, world!"
|
| 665 |
|
|
@@ -671,16 +671,23 @@ class TestPromptDecorator:
|
|
| 671 |
content = await prompt.render()
|
| 672 |
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
| 673 |
|
| 674 |
-
async def
|
| 675 |
mcp = FastMCP()
|
| 676 |
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
):
|
|
|
|
| 680 |
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
|
| 685 |
async def test_prompt_decorator_with_name(self):
|
| 686 |
mcp = FastMCP()
|
|
@@ -713,7 +720,7 @@ class TestPromptDecorator:
|
|
| 713 |
async def test_prompt_decorator_with_parameters(self):
|
| 714 |
mcp = FastMCP()
|
| 715 |
|
| 716 |
-
@mcp.prompt
|
| 717 |
def test_prompt(name: str, greeting: str = "Hello") -> str:
|
| 718 |
return f"{greeting}, {name}!"
|
| 719 |
|
|
@@ -782,7 +789,7 @@ class TestPromptDecorator:
|
|
| 782 |
|
| 783 |
class MyClass:
|
| 784 |
@staticmethod
|
| 785 |
-
@mcp.prompt
|
| 786 |
def test_prompt() -> str:
|
| 787 |
return "Static Hello, world!"
|
| 788 |
|
|
@@ -795,7 +802,7 @@ class TestPromptDecorator:
|
|
| 795 |
async def test_prompt_decorator_async_function(self):
|
| 796 |
mcp = FastMCP()
|
| 797 |
|
| 798 |
-
@mcp.prompt
|
| 799 |
async def test_prompt() -> str:
|
| 800 |
return "Async Hello, world!"
|
| 801 |
|
|
@@ -818,6 +825,63 @@ class TestPromptDecorator:
|
|
| 818 |
prompt = prompts_dict["sample_prompt"]
|
| 819 |
assert prompt.tags == {"example", "test-tag"}
|
| 820 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 821 |
|
| 822 |
class TestResourcePrefixHelpers:
|
| 823 |
@pytest.mark.parametrize(
|
|
|
|
| 659 |
async def test_prompt_decorator(self):
|
| 660 |
mcp = FastMCP()
|
| 661 |
|
| 662 |
+
@mcp.prompt
|
| 663 |
def fn() -> str:
|
| 664 |
return "Hello, world!"
|
| 665 |
|
|
|
|
| 671 |
content = await prompt.render()
|
| 672 |
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
| 673 |
|
| 674 |
+
async def test_prompt_decorator_without_parentheses(self):
|
| 675 |
mcp = FastMCP()
|
| 676 |
|
| 677 |
+
# This should now work correctly (not raise an error)
|
| 678 |
+
@mcp.prompt # No parentheses - this is now supported
|
| 679 |
+
def fn() -> str:
|
| 680 |
+
return "Hello, world!"
|
| 681 |
|
| 682 |
+
# Verify the prompt was registered correctly
|
| 683 |
+
prompts = await mcp.get_prompts()
|
| 684 |
+
assert "fn" in prompts
|
| 685 |
+
|
| 686 |
+
# Verify it can be called
|
| 687 |
+
async with Client(mcp) as client:
|
| 688 |
+
result = await client.get_prompt("fn")
|
| 689 |
+
assert len(result.messages) == 1
|
| 690 |
+
assert result.messages[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
| 691 |
|
| 692 |
async def test_prompt_decorator_with_name(self):
|
| 693 |
mcp = FastMCP()
|
|
|
|
| 720 |
async def test_prompt_decorator_with_parameters(self):
|
| 721 |
mcp = FastMCP()
|
| 722 |
|
| 723 |
+
@mcp.prompt
|
| 724 |
def test_prompt(name: str, greeting: str = "Hello") -> str:
|
| 725 |
return f"{greeting}, {name}!"
|
| 726 |
|
|
|
|
| 789 |
|
| 790 |
class MyClass:
|
| 791 |
@staticmethod
|
| 792 |
+
@mcp.prompt
|
| 793 |
def test_prompt() -> str:
|
| 794 |
return "Static Hello, world!"
|
| 795 |
|
|
|
|
| 802 |
async def test_prompt_decorator_async_function(self):
|
| 803 |
mcp = FastMCP()
|
| 804 |
|
| 805 |
+
@mcp.prompt
|
| 806 |
async def test_prompt() -> str:
|
| 807 |
return "Async Hello, world!"
|
| 808 |
|
|
|
|
| 825 |
prompt = prompts_dict["sample_prompt"]
|
| 826 |
assert prompt.tags == {"example", "test-tag"}
|
| 827 |
|
| 828 |
+
async def test_prompt_decorator_with_string_name(self):
|
| 829 |
+
"""Test that @prompt(\"custom_name\") syntax works correctly."""
|
| 830 |
+
mcp = FastMCP()
|
| 831 |
+
|
| 832 |
+
@mcp.prompt("string_named_prompt")
|
| 833 |
+
def my_function() -> str:
|
| 834 |
+
"""A function with a string name."""
|
| 835 |
+
return "Hello from string named prompt!"
|
| 836 |
+
|
| 837 |
+
# Verify the prompt was registered with the custom name
|
| 838 |
+
prompts = await mcp.get_prompts()
|
| 839 |
+
assert "string_named_prompt" in prompts
|
| 840 |
+
assert "my_function" not in prompts # Original name should not be registered
|
| 841 |
+
|
| 842 |
+
# Verify it can be called
|
| 843 |
+
async with Client(mcp) as client:
|
| 844 |
+
result = await client.get_prompt("string_named_prompt")
|
| 845 |
+
assert len(result.messages) == 1
|
| 846 |
+
assert result.messages[0].content.text == "Hello from string named prompt!" # type: ignore[attr-defined]
|
| 847 |
+
|
| 848 |
+
async def test_prompt_direct_function_call(self):
|
| 849 |
+
"""Test that prompts can be registered via direct function call."""
|
| 850 |
+
mcp = FastMCP()
|
| 851 |
+
|
| 852 |
+
def standalone_function() -> str:
|
| 853 |
+
"""A standalone function to be registered."""
|
| 854 |
+
return "Hello from direct call!"
|
| 855 |
+
|
| 856 |
+
# Register it directly using the new syntax
|
| 857 |
+
result_fn = mcp.prompt(standalone_function, name="direct_call_prompt")
|
| 858 |
+
|
| 859 |
+
# The function should be returned unchanged
|
| 860 |
+
assert result_fn is standalone_function
|
| 861 |
+
|
| 862 |
+
# Verify the prompt was registered correctly
|
| 863 |
+
prompts = await mcp.get_prompts()
|
| 864 |
+
assert "direct_call_prompt" in prompts
|
| 865 |
+
|
| 866 |
+
# Verify it can be called
|
| 867 |
+
async with Client(mcp) as client:
|
| 868 |
+
result = await client.get_prompt("direct_call_prompt")
|
| 869 |
+
assert len(result.messages) == 1
|
| 870 |
+
assert result.messages[0].content.text == "Hello from direct call!" # type: ignore[attr-defined]
|
| 871 |
+
|
| 872 |
+
async def test_prompt_decorator_conflicting_names_error(self):
|
| 873 |
+
"""Test that providing both positional and keyword names raises an error."""
|
| 874 |
+
mcp = FastMCP()
|
| 875 |
+
|
| 876 |
+
with pytest.raises(
|
| 877 |
+
TypeError,
|
| 878 |
+
match="Cannot specify both a name as first argument and as keyword argument",
|
| 879 |
+
):
|
| 880 |
+
|
| 881 |
+
@mcp.prompt("positional_name", name="keyword_name")
|
| 882 |
+
def my_function() -> str:
|
| 883 |
+
return "Hello, world!"
|
| 884 |
+
|
| 885 |
|
| 886 |
class TestResourcePrefixHelpers:
|
| 887 |
@pytest.mark.parametrize(
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -1091,7 +1091,7 @@ class TestPrompts:
|
|
| 1091 |
"""Test that the prompt decorator registers prompts correctly."""
|
| 1092 |
mcp = FastMCP()
|
| 1093 |
|
| 1094 |
-
@mcp.prompt
|
| 1095 |
def fn() -> str:
|
| 1096 |
return "Hello, world!"
|
| 1097 |
|
|
@@ -1133,20 +1133,23 @@ class TestPrompts:
|
|
| 1133 |
content = await prompt.render()
|
| 1134 |
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
| 1135 |
|
| 1136 |
-
def
|
| 1137 |
-
"""Test error when decorator is used incorrectly."""
|
| 1138 |
mcp = FastMCP()
|
| 1139 |
-
with pytest.raises(TypeError, match="decorator was used incorrectly"):
|
| 1140 |
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1144 |
|
| 1145 |
async def test_list_prompts(self):
|
| 1146 |
"""Test listing prompts through MCP protocol."""
|
| 1147 |
mcp = FastMCP()
|
| 1148 |
|
| 1149 |
-
@mcp.prompt
|
| 1150 |
def fn(name: str, optional: str = "default") -> str:
|
| 1151 |
return f"Hello, {name}! {optional}"
|
| 1152 |
|
|
@@ -1169,7 +1172,7 @@ class TestPrompts:
|
|
| 1169 |
"""Test getting a prompt through MCP protocol."""
|
| 1170 |
mcp = FastMCP()
|
| 1171 |
|
| 1172 |
-
@mcp.prompt
|
| 1173 |
def fn(name: str) -> str:
|
| 1174 |
return f"Hello, {name}!"
|
| 1175 |
|
|
@@ -1185,7 +1188,7 @@ class TestPrompts:
|
|
| 1185 |
"""Test getting a prompt that returns resource content."""
|
| 1186 |
mcp = FastMCP()
|
| 1187 |
|
| 1188 |
-
@mcp.prompt
|
| 1189 |
def fn() -> PromptMessage:
|
| 1190 |
return PromptMessage(
|
| 1191 |
role="user",
|
|
@@ -1220,7 +1223,7 @@ class TestPrompts:
|
|
| 1220 |
"""Test error when required arguments are missing."""
|
| 1221 |
mcp = FastMCP()
|
| 1222 |
|
| 1223 |
-
@mcp.prompt
|
| 1224 |
def prompt_fn(name: str) -> str:
|
| 1225 |
return f"Hello, {name}!"
|
| 1226 |
|
|
@@ -1271,7 +1274,7 @@ class TestPromptContext:
|
|
| 1271 |
async def test_prompt_context(self):
|
| 1272 |
mcp = FastMCP()
|
| 1273 |
|
| 1274 |
-
@mcp.prompt
|
| 1275 |
def prompt_fn(name: str, ctx: Context) -> str:
|
| 1276 |
assert isinstance(ctx, Context)
|
| 1277 |
return f"Hello, {name}! {ctx.request_id}"
|
|
|
|
| 1091 |
"""Test that the prompt decorator registers prompts correctly."""
|
| 1092 |
mcp = FastMCP()
|
| 1093 |
|
| 1094 |
+
@mcp.prompt
|
| 1095 |
def fn() -> str:
|
| 1096 |
return "Hello, world!"
|
| 1097 |
|
|
|
|
| 1133 |
content = await prompt.render()
|
| 1134 |
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
| 1135 |
|
| 1136 |
+
async def test_prompt_decorator_with_parens(self):
|
|
|
|
| 1137 |
mcp = FastMCP()
|
|
|
|
| 1138 |
|
| 1139 |
+
@mcp.prompt()
|
| 1140 |
+
def fn() -> str:
|
| 1141 |
+
return "Hello, world!"
|
| 1142 |
+
|
| 1143 |
+
prompts_dict = await mcp.get_prompts()
|
| 1144 |
+
assert len(prompts_dict) == 1
|
| 1145 |
+
prompt = prompts_dict["fn"]
|
| 1146 |
+
assert prompt.name == "fn"
|
| 1147 |
|
| 1148 |
async def test_list_prompts(self):
|
| 1149 |
"""Test listing prompts through MCP protocol."""
|
| 1150 |
mcp = FastMCP()
|
| 1151 |
|
| 1152 |
+
@mcp.prompt
|
| 1153 |
def fn(name: str, optional: str = "default") -> str:
|
| 1154 |
return f"Hello, {name}! {optional}"
|
| 1155 |
|
|
|
|
| 1172 |
"""Test getting a prompt through MCP protocol."""
|
| 1173 |
mcp = FastMCP()
|
| 1174 |
|
| 1175 |
+
@mcp.prompt
|
| 1176 |
def fn(name: str) -> str:
|
| 1177 |
return f"Hello, {name}!"
|
| 1178 |
|
|
|
|
| 1188 |
"""Test getting a prompt that returns resource content."""
|
| 1189 |
mcp = FastMCP()
|
| 1190 |
|
| 1191 |
+
@mcp.prompt
|
| 1192 |
def fn() -> PromptMessage:
|
| 1193 |
return PromptMessage(
|
| 1194 |
role="user",
|
|
|
|
| 1223 |
"""Test error when required arguments are missing."""
|
| 1224 |
mcp = FastMCP()
|
| 1225 |
|
| 1226 |
+
@mcp.prompt
|
| 1227 |
def prompt_fn(name: str) -> str:
|
| 1228 |
return f"Hello, {name}!"
|
| 1229 |
|
|
|
|
| 1274 |
async def test_prompt_context(self):
|
| 1275 |
mcp = FastMCP()
|
| 1276 |
|
| 1277 |
+
@mcp.prompt
|
| 1278 |
def prompt_fn(name: str, ctx: Context) -> str:
|
| 1279 |
assert isinstance(ctx, Context)
|
| 1280 |
return f"Hello, {name}! {ctx.request_id}"
|
tests/test_servers/fastmcp_server.py
CHANGED
|
@@ -53,6 +53,6 @@ async def get_user(user_id: str) -> dict[str, Any] | None:
|
|
| 53 |
# --- Prompts ---
|
| 54 |
|
| 55 |
|
| 56 |
-
@server.prompt
|
| 57 |
def welcome(name: str) -> str:
|
| 58 |
return f"Welcome to FastMCP, {name}!"
|
|
|
|
| 53 |
# --- Prompts ---
|
| 54 |
|
| 55 |
|
| 56 |
+
@server.prompt
|
| 57 |
def welcome(name: str) -> str:
|
| 58 |
return f"Welcome to FastMCP, {name}!"
|