mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
155 lines
5.4 KiB
Python
155 lines
5.4 KiB
Python
"""Tests for the standalone @prompt decorator.
|
|
|
|
The @prompt decorator attaches metadata to functions without registering them
|
|
to a server. Functions can be added explicitly via server.add_prompt() or
|
|
discovered by FileSystemProvider.
|
|
"""
|
|
|
|
from typing import cast
|
|
|
|
import pytest
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.client import Client
|
|
from fastmcp.prompts import prompt
|
|
from fastmcp.prompts.function_prompt import DecoratedPrompt, PromptMeta
|
|
|
|
|
|
class TestPromptDecorator:
|
|
"""Tests for the @prompt decorator."""
|
|
|
|
def test_prompt_without_parens(self):
|
|
"""@prompt without parentheses should attach metadata."""
|
|
|
|
@prompt
|
|
def analyze(topic: str) -> str:
|
|
return f"Analyze: {topic}"
|
|
|
|
decorated = cast(DecoratedPrompt, analyze)
|
|
assert callable(analyze)
|
|
assert hasattr(analyze, "__fastmcp__")
|
|
assert isinstance(decorated.__fastmcp__, PromptMeta)
|
|
assert decorated.__fastmcp__.name is None # Uses function name by default
|
|
|
|
def test_prompt_with_empty_parens(self):
|
|
"""@prompt() with empty parentheses should attach metadata."""
|
|
|
|
@prompt()
|
|
def analyze(topic: str) -> str:
|
|
return f"Analyze: {topic}"
|
|
|
|
decorated = cast(DecoratedPrompt, analyze)
|
|
assert callable(analyze)
|
|
assert hasattr(analyze, "__fastmcp__")
|
|
assert isinstance(decorated.__fastmcp__, PromptMeta)
|
|
|
|
def test_prompt_with_name_arg(self):
|
|
"""@prompt("name") with name as first arg should work."""
|
|
|
|
@prompt("custom-analyze")
|
|
def analyze(topic: str) -> str:
|
|
return f"Analyze: {topic}"
|
|
|
|
decorated = cast(DecoratedPrompt, analyze)
|
|
assert callable(analyze)
|
|
assert hasattr(analyze, "__fastmcp__")
|
|
assert decorated.__fastmcp__.name == "custom-analyze"
|
|
|
|
def test_prompt_with_name_kwarg(self):
|
|
"""@prompt(name="name") with keyword arg should work."""
|
|
|
|
@prompt(name="custom-analyze")
|
|
def analyze(topic: str) -> str:
|
|
return f"Analyze: {topic}"
|
|
|
|
decorated = cast(DecoratedPrompt, analyze)
|
|
assert callable(analyze)
|
|
assert hasattr(analyze, "__fastmcp__")
|
|
assert decorated.__fastmcp__.name == "custom-analyze"
|
|
|
|
def test_prompt_with_all_metadata(self):
|
|
"""@prompt with all metadata should store it all."""
|
|
|
|
@prompt(
|
|
name="custom-analyze",
|
|
title="Analysis Prompt",
|
|
description="Analyzes topics",
|
|
tags={"analysis", "demo"},
|
|
meta={"custom": "value"},
|
|
)
|
|
def analyze(topic: str) -> str:
|
|
return f"Analyze: {topic}"
|
|
|
|
decorated = cast(DecoratedPrompt, analyze)
|
|
assert callable(analyze)
|
|
assert hasattr(analyze, "__fastmcp__")
|
|
assert decorated.__fastmcp__.name == "custom-analyze"
|
|
assert decorated.__fastmcp__.title == "Analysis Prompt"
|
|
assert decorated.__fastmcp__.description == "Analyzes topics"
|
|
assert decorated.__fastmcp__.tags == {"analysis", "demo"}
|
|
assert decorated.__fastmcp__.meta == {"custom": "value"}
|
|
|
|
async def test_prompt_function_still_callable(self):
|
|
"""Decorated function should still be directly callable."""
|
|
|
|
@prompt
|
|
def analyze(topic: str) -> str:
|
|
"""Analyze a topic."""
|
|
return f"Please analyze: {topic}"
|
|
|
|
# The function is still callable even though it has metadata
|
|
result = cast(DecoratedPrompt, analyze)("Python")
|
|
assert result == "Please analyze: Python"
|
|
|
|
async def test_staticmethod_metadata_is_available_on_unwrapped_function(self):
|
|
"""@prompt should attach metadata where staticmethod access can find it."""
|
|
|
|
class MyClass:
|
|
@prompt(name="custom-static-prompt")
|
|
@staticmethod
|
|
def my_prompt() -> str:
|
|
return "hello"
|
|
|
|
mcp = FastMCP("Test")
|
|
mcp.add_prompt(MyClass.my_prompt)
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
|
|
assert any(p.name == "custom-static-prompt" for p in prompts)
|
|
|
|
def test_prompt_rejects_classmethod_decorator(self):
|
|
"""@prompt should reject classmethod-decorated functions."""
|
|
with pytest.raises(TypeError, match="classmethod"):
|
|
|
|
class MyClass:
|
|
@prompt
|
|
@classmethod
|
|
def my_prompt(cls, topic: str) -> str:
|
|
return f"Analyze: {topic}"
|
|
|
|
def test_prompt_with_both_name_args_raises(self):
|
|
"""@prompt should raise if both positional and keyword name are given."""
|
|
with pytest.raises(TypeError, match="Cannot specify.*both.*argument.*keyword"):
|
|
|
|
@prompt("name1", name="name2") # type: ignore[call-overload] # ty:ignore[invalid-argument-type]
|
|
def my_prompt() -> str:
|
|
return "hello"
|
|
|
|
async def test_prompt_added_to_server(self):
|
|
"""Prompt created by @prompt should work when added to a server."""
|
|
|
|
@prompt
|
|
def analyze(topic: str) -> str:
|
|
"""Analyze a topic."""
|
|
return f"Please analyze: {topic}"
|
|
|
|
mcp = FastMCP("Test")
|
|
mcp.add_prompt(analyze)
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert any(p.name == "analyze" for p in prompts)
|
|
|
|
result = await client.get_prompt("analyze", {"topic": "FastMCP"})
|
|
assert "FastMCP" in str(result)
|