Spaces:
Running
Running
Merge pull request #701 from jlowin/prompt-strict
Browse filesUse strict basemodel for Prompt; relax from_function deprecation
- src/fastmcp/prompts/prompt.py +62 -27
- src/fastmcp/prompts/prompt_manager.py +6 -4
- src/fastmcp/server/proxy.py +2 -1
- src/fastmcp/server/server.py +2 -2
- src/fastmcp/tools/tool.py +18 -6
- src/fastmcp/tools/tool_manager.py +2 -2
- tests/deprecated/test_tool_from_function_deprecated.py +0 -76
- tests/prompts/test_prompt_manager.py +4 -1
- tests/tools/test_tool.py +23 -23
- tests/tools/test_tool_manager.py +3 -3
src/fastmcp/prompts/prompt.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
from __future__ import annotations as _annotations
|
| 4 |
|
| 5 |
import inspect
|
|
|
|
| 6 |
from collections.abc import Awaitable, Callable, Sequence
|
| 7 |
from typing import TYPE_CHECKING, Annotated, Any
|
| 8 |
|
|
@@ -10,13 +11,14 @@ import pydantic_core
|
|
| 10 |
from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
|
| 11 |
from mcp.types import Prompt as MCPPrompt
|
| 12 |
from mcp.types import PromptArgument as MCPPromptArgument
|
| 13 |
-
from pydantic import
|
| 14 |
|
| 15 |
from fastmcp.exceptions import PromptError
|
| 16 |
from fastmcp.server.dependencies import get_context
|
| 17 |
from fastmcp.utilities.json_schema import compress_schema
|
| 18 |
from fastmcp.utilities.logging import get_logger
|
| 19 |
from fastmcp.utilities.types import (
|
|
|
|
| 20 |
_convert_set_defaults,
|
| 21 |
find_kwarg_by_type,
|
| 22 |
get_cached_typeadapter,
|
|
@@ -52,7 +54,7 @@ SyncPromptResult = (
|
|
| 52 |
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
|
| 53 |
|
| 54 |
|
| 55 |
-
class PromptArgument(
|
| 56 |
"""An argument that can be passed to a prompt."""
|
| 57 |
|
| 58 |
name: str = Field(description="Name of the argument")
|
|
@@ -64,7 +66,7 @@ class PromptArgument(BaseModel):
|
|
| 64 |
)
|
| 65 |
|
| 66 |
|
| 67 |
-
class Prompt(
|
| 68 |
"""A prompt template that can be rendered with parameters."""
|
| 69 |
|
| 70 |
name: str = Field(description="Name of the prompt")
|
|
@@ -77,6 +79,61 @@ class Prompt(BaseModel):
|
|
| 77 |
arguments: list[PromptArgument] | None = Field(
|
| 78 |
None, description="Arguments that can be passed to the prompt"
|
| 79 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
| 81 |
|
| 82 |
@classmethod
|
|
@@ -86,7 +143,7 @@ class Prompt(BaseModel):
|
|
| 86 |
name: str | None = None,
|
| 87 |
description: str | None = None,
|
| 88 |
tags: set[str] | None = None,
|
| 89 |
-
) ->
|
| 90 |
"""Create a Prompt from a function.
|
| 91 |
|
| 92 |
The function can return:
|
|
@@ -147,8 +204,8 @@ class Prompt(BaseModel):
|
|
| 147 |
name=func_name,
|
| 148 |
description=description,
|
| 149 |
arguments=arguments,
|
| 150 |
-
fn=fn,
|
| 151 |
tags=tags or set(),
|
|
|
|
| 152 |
)
|
| 153 |
|
| 154 |
async def render(
|
|
@@ -212,25 +269,3 @@ class Prompt(BaseModel):
|
|
| 212 |
except Exception as e:
|
| 213 |
logger.exception(f"Error rendering prompt {self.name}: {e}")
|
| 214 |
raise PromptError(f"Error rendering prompt {self.name}.")
|
| 215 |
-
|
| 216 |
-
def __eq__(self, other: object) -> bool:
|
| 217 |
-
if not isinstance(other, Prompt):
|
| 218 |
-
return False
|
| 219 |
-
return self.model_dump() == other.model_dump()
|
| 220 |
-
|
| 221 |
-
def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
|
| 222 |
-
"""Convert the prompt to an MCP prompt."""
|
| 223 |
-
arguments = [
|
| 224 |
-
MCPPromptArgument(
|
| 225 |
-
name=arg.name,
|
| 226 |
-
description=arg.description,
|
| 227 |
-
required=arg.required,
|
| 228 |
-
)
|
| 229 |
-
for arg in self.arguments or []
|
| 230 |
-
]
|
| 231 |
-
kwargs = {
|
| 232 |
-
"name": self.name,
|
| 233 |
-
"description": self.description,
|
| 234 |
-
"arguments": arguments,
|
| 235 |
-
}
|
| 236 |
-
return MCPPrompt(**kwargs | overrides)
|
|
|
|
| 3 |
from __future__ import annotations as _annotations
|
| 4 |
|
| 5 |
import inspect
|
| 6 |
+
from abc import ABC, abstractmethod
|
| 7 |
from collections.abc import Awaitable, Callable, Sequence
|
| 8 |
from typing import TYPE_CHECKING, Annotated, Any
|
| 9 |
|
|
|
|
| 11 |
from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
|
| 12 |
from mcp.types import Prompt as MCPPrompt
|
| 13 |
from mcp.types import PromptArgument as MCPPromptArgument
|
| 14 |
+
from pydantic import BeforeValidator, Field, TypeAdapter, validate_call
|
| 15 |
|
| 16 |
from fastmcp.exceptions import PromptError
|
| 17 |
from fastmcp.server.dependencies import get_context
|
| 18 |
from fastmcp.utilities.json_schema import compress_schema
|
| 19 |
from fastmcp.utilities.logging import get_logger
|
| 20 |
from fastmcp.utilities.types import (
|
| 21 |
+
FastMCPBaseModel,
|
| 22 |
_convert_set_defaults,
|
| 23 |
find_kwarg_by_type,
|
| 24 |
get_cached_typeadapter,
|
|
|
|
| 54 |
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
|
| 55 |
|
| 56 |
|
| 57 |
+
class PromptArgument(FastMCPBaseModel):
|
| 58 |
"""An argument that can be passed to a prompt."""
|
| 59 |
|
| 60 |
name: str = Field(description="Name of the argument")
|
|
|
|
| 66 |
)
|
| 67 |
|
| 68 |
|
| 69 |
+
class Prompt(FastMCPBaseModel, ABC):
|
| 70 |
"""A prompt template that can be rendered with parameters."""
|
| 71 |
|
| 72 |
name: str = Field(description="Name of the prompt")
|
|
|
|
| 79 |
arguments: list[PromptArgument] | None = Field(
|
| 80 |
None, description="Arguments that can be passed to the prompt"
|
| 81 |
)
|
| 82 |
+
|
| 83 |
+
def __eq__(self, other: object) -> bool:
|
| 84 |
+
if type(self) is not type(other):
|
| 85 |
+
return False
|
| 86 |
+
assert isinstance(other, type(self))
|
| 87 |
+
return self.model_dump() == other.model_dump()
|
| 88 |
+
|
| 89 |
+
def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
|
| 90 |
+
"""Convert the prompt to an MCP prompt."""
|
| 91 |
+
arguments = [
|
| 92 |
+
MCPPromptArgument(
|
| 93 |
+
name=arg.name,
|
| 94 |
+
description=arg.description,
|
| 95 |
+
required=arg.required,
|
| 96 |
+
)
|
| 97 |
+
for arg in self.arguments or []
|
| 98 |
+
]
|
| 99 |
+
kwargs = {
|
| 100 |
+
"name": self.name,
|
| 101 |
+
"description": self.description,
|
| 102 |
+
"arguments": arguments,
|
| 103 |
+
}
|
| 104 |
+
return MCPPrompt(**kwargs | overrides)
|
| 105 |
+
|
| 106 |
+
@staticmethod
|
| 107 |
+
def from_function(
|
| 108 |
+
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
|
| 109 |
+
name: str | None = None,
|
| 110 |
+
description: str | None = None,
|
| 111 |
+
tags: set[str] | None = None,
|
| 112 |
+
) -> FunctionPrompt:
|
| 113 |
+
"""Create a Prompt from a function.
|
| 114 |
+
|
| 115 |
+
The function can return:
|
| 116 |
+
- A string (converted to a message)
|
| 117 |
+
- A Message object
|
| 118 |
+
- A dict (converted to a message)
|
| 119 |
+
- A sequence of any of the above
|
| 120 |
+
"""
|
| 121 |
+
return FunctionPrompt.from_function(
|
| 122 |
+
fn=fn, name=name, description=description, tags=tags
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
@abstractmethod
|
| 126 |
+
async def render(
|
| 127 |
+
self,
|
| 128 |
+
arguments: dict[str, Any] | None = None,
|
| 129 |
+
) -> list[PromptMessage]:
|
| 130 |
+
"""Render the prompt with arguments."""
|
| 131 |
+
raise NotImplementedError("Prompt.render() must be implemented by subclasses")
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class FunctionPrompt(Prompt):
|
| 135 |
+
"""A prompt that is a function."""
|
| 136 |
+
|
| 137 |
fn: Callable[..., PromptResult | Awaitable[PromptResult]]
|
| 138 |
|
| 139 |
@classmethod
|
|
|
|
| 143 |
name: str | None = None,
|
| 144 |
description: str | None = None,
|
| 145 |
tags: set[str] | None = None,
|
| 146 |
+
) -> FunctionPrompt:
|
| 147 |
"""Create a Prompt from a function.
|
| 148 |
|
| 149 |
The function can return:
|
|
|
|
| 204 |
name=func_name,
|
| 205 |
description=description,
|
| 206 |
arguments=arguments,
|
|
|
|
| 207 |
tags=tags or set(),
|
| 208 |
+
fn=fn,
|
| 209 |
)
|
| 210 |
|
| 211 |
async def render(
|
|
|
|
| 269 |
except Exception as e:
|
| 270 |
logger.exception(f"Error rendering prompt {self.name}: {e}")
|
| 271 |
raise PromptError(f"Error rendering prompt {self.name}.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/fastmcp/prompts/prompt_manager.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|
| 8 |
from mcp import GetPromptResult
|
| 9 |
|
| 10 |
from fastmcp.exceptions import NotFoundError, PromptError
|
| 11 |
-
from fastmcp.prompts.prompt import Prompt, PromptResult
|
| 12 |
from fastmcp.settings import DuplicateBehavior
|
| 13 |
from fastmcp.utilities.logging import get_logger
|
| 14 |
|
|
@@ -55,10 +55,12 @@ class PromptManager:
|
|
| 55 |
name: str | None = None,
|
| 56 |
description: str | None = None,
|
| 57 |
tags: set[str] | None = None,
|
| 58 |
-
) ->
|
| 59 |
"""Create a prompt from a function."""
|
| 60 |
-
prompt =
|
| 61 |
-
|
|
|
|
|
|
|
| 62 |
|
| 63 |
def add_prompt(self, prompt: Prompt, key: str | None = None) -> Prompt:
|
| 64 |
"""Add a prompt to the manager."""
|
|
|
|
| 8 |
from mcp import GetPromptResult
|
| 9 |
|
| 10 |
from fastmcp.exceptions import NotFoundError, PromptError
|
| 11 |
+
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
| 12 |
from fastmcp.settings import DuplicateBehavior
|
| 13 |
from fastmcp.utilities.logging import get_logger
|
| 14 |
|
|
|
|
| 55 |
name: str | None = None,
|
| 56 |
description: str | None = None,
|
| 57 |
tags: set[str] | None = None,
|
| 58 |
+
) -> FunctionPrompt:
|
| 59 |
"""Create a prompt from a function."""
|
| 60 |
+
prompt = FunctionPrompt.from_function(
|
| 61 |
+
fn, name=name, description=description, tags=tags
|
| 62 |
+
)
|
| 63 |
+
return self.add_prompt(prompt) # type: ignore
|
| 64 |
|
| 65 |
def add_prompt(self, prompt: Prompt, key: str | None = None) -> Prompt:
|
| 66 |
"""Add a prompt to the manager."""
|
src/fastmcp/server/proxy.py
CHANGED
|
@@ -153,6 +153,8 @@ class ProxyTemplate(ResourceTemplate):
|
|
| 153 |
|
| 154 |
|
| 155 |
class ProxyPrompt(Prompt):
|
|
|
|
|
|
|
| 156 |
def __init__(self, client: Client, **kwargs):
|
| 157 |
super().__init__(**kwargs)
|
| 158 |
self._client = client
|
|
@@ -164,7 +166,6 @@ class ProxyPrompt(Prompt):
|
|
| 164 |
name=prompt.name,
|
| 165 |
description=prompt.description,
|
| 166 |
arguments=[a.model_dump() for a in prompt.arguments or []],
|
| 167 |
-
fn=_proxy_passthrough,
|
| 168 |
)
|
| 169 |
|
| 170 |
async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
|
|
|
|
| 153 |
|
| 154 |
|
| 155 |
class ProxyPrompt(Prompt):
|
| 156 |
+
_client: Client
|
| 157 |
+
|
| 158 |
def __init__(self, client: Client, **kwargs):
|
| 159 |
super().__init__(**kwargs)
|
| 160 |
self._client = client
|
|
|
|
| 166 |
name=prompt.name,
|
| 167 |
description=prompt.description,
|
| 168 |
arguments=[a.model_dump() for a in prompt.arguments or []],
|
|
|
|
| 169 |
)
|
| 170 |
|
| 171 |
async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
|
src/fastmcp/server/server.py
CHANGED
|
@@ -55,7 +55,7 @@ from fastmcp.server.http import (
|
|
| 55 |
create_streamable_http_app,
|
| 56 |
)
|
| 57 |
from fastmcp.tools import ToolManager
|
| 58 |
-
from fastmcp.tools.tool import
|
| 59 |
from fastmcp.utilities.cache import TimedCache
|
| 60 |
from fastmcp.utilities.decorators import DecoratedFunction
|
| 61 |
from fastmcp.utilities.logging import get_logger
|
|
@@ -508,7 +508,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 508 |
if isinstance(annotations, dict):
|
| 509 |
annotations = ToolAnnotations(**annotations)
|
| 510 |
|
| 511 |
-
tool =
|
| 512 |
fn,
|
| 513 |
name=name,
|
| 514 |
description=description,
|
|
|
|
| 55 |
create_streamable_http_app,
|
| 56 |
)
|
| 57 |
from fastmcp.tools import ToolManager
|
| 58 |
+
from fastmcp.tools.tool import Tool
|
| 59 |
from fastmcp.utilities.cache import TimedCache
|
| 60 |
from fastmcp.utilities.decorators import DecoratedFunction
|
| 61 |
from fastmcp.utilities.logging import get_logger
|
|
|
|
| 508 |
if isinstance(annotations, dict):
|
| 509 |
annotations = ToolAnnotations(**annotations)
|
| 510 |
|
| 511 |
+
tool = Tool.from_function(
|
| 512 |
fn,
|
| 513 |
name=name,
|
| 514 |
description=description,
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import inspect
|
| 4 |
import json
|
| 5 |
-
import warnings
|
| 6 |
from abc import ABC, abstractmethod
|
| 7 |
from collections.abc import Callable
|
| 8 |
from typing import TYPE_CHECKING, Annotated, Any
|
|
@@ -66,12 +65,25 @@ class Tool(FastMCPBaseModel, ABC):
|
|
| 66 |
return MCPTool(**kwargs | overrides)
|
| 67 |
|
| 68 |
@staticmethod
|
| 69 |
-
def from_function(
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
)
|
| 74 |
-
return FunctionTool.from_function(fn, **overrides)
|
| 75 |
|
| 76 |
def __eq__(self, other: object) -> bool:
|
| 77 |
if not isinstance(other, Tool):
|
|
|
|
| 2 |
|
| 3 |
import inspect
|
| 4 |
import json
|
|
|
|
| 5 |
from abc import ABC, abstractmethod
|
| 6 |
from collections.abc import Callable
|
| 7 |
from typing import TYPE_CHECKING, Annotated, Any
|
|
|
|
| 65 |
return MCPTool(**kwargs | overrides)
|
| 66 |
|
| 67 |
@staticmethod
|
| 68 |
+
def from_function(
|
| 69 |
+
fn: Callable[..., Any],
|
| 70 |
+
name: str | None = None,
|
| 71 |
+
description: str | None = None,
|
| 72 |
+
tags: set[str] | None = None,
|
| 73 |
+
annotations: ToolAnnotations | None = None,
|
| 74 |
+
exclude_args: list[str] | None = None,
|
| 75 |
+
serializer: Callable[[Any], str] | None = None,
|
| 76 |
+
) -> FunctionTool:
|
| 77 |
+
"""Create a Tool from a function."""
|
| 78 |
+
return FunctionTool.from_function(
|
| 79 |
+
fn=fn,
|
| 80 |
+
name=name,
|
| 81 |
+
description=description,
|
| 82 |
+
tags=tags,
|
| 83 |
+
annotations=annotations,
|
| 84 |
+
exclude_args=exclude_args,
|
| 85 |
+
serializer=serializer,
|
| 86 |
)
|
|
|
|
| 87 |
|
| 88 |
def __eq__(self, other: object) -> bool:
|
| 89 |
if not isinstance(other, Tool):
|
src/fastmcp/tools/tool_manager.py
CHANGED
|
@@ -7,7 +7,7 @@ from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotatio
|
|
| 7 |
|
| 8 |
from fastmcp.exceptions import NotFoundError, ToolError
|
| 9 |
from fastmcp.settings import DuplicateBehavior
|
| 10 |
-
from fastmcp.tools.tool import
|
| 11 |
from fastmcp.utilities.logging import get_logger
|
| 12 |
|
| 13 |
if TYPE_CHECKING:
|
|
@@ -69,7 +69,7 @@ class ToolManager:
|
|
| 69 |
exclude_args: list[str] | None = None,
|
| 70 |
) -> Tool:
|
| 71 |
"""Add a tool to the server."""
|
| 72 |
-
tool =
|
| 73 |
fn,
|
| 74 |
name=name,
|
| 75 |
description=description,
|
|
|
|
| 7 |
|
| 8 |
from fastmcp.exceptions import NotFoundError, ToolError
|
| 9 |
from fastmcp.settings import DuplicateBehavior
|
| 10 |
+
from fastmcp.tools.tool import Tool
|
| 11 |
from fastmcp.utilities.logging import get_logger
|
| 12 |
|
| 13 |
if TYPE_CHECKING:
|
|
|
|
| 69 |
exclude_args: list[str] | None = None,
|
| 70 |
) -> Tool:
|
| 71 |
"""Add a tool to the server."""
|
| 72 |
+
tool = Tool.from_function(
|
| 73 |
fn,
|
| 74 |
name=name,
|
| 75 |
description=description,
|
tests/deprecated/test_tool_from_function_deprecated.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
| 1 |
-
"""Tests for deprecated Tool.from_function() method.
|
| 2 |
-
|
| 3 |
-
The Tool.from_function() method was deprecated in version 2.6.2 in favor of
|
| 4 |
-
FunctionTool.from_function().
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import warnings
|
| 8 |
-
|
| 9 |
-
import pytest
|
| 10 |
-
|
| 11 |
-
from fastmcp.tools.tool import FunctionTool, Tool
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def test_tool_from_function_deprecation_warning():
|
| 15 |
-
"""Test that Tool.from_function() raises a deprecation warning."""
|
| 16 |
-
|
| 17 |
-
def example_function(x: int) -> str:
|
| 18 |
-
"""Example function for testing."""
|
| 19 |
-
return f"Result: {x}"
|
| 20 |
-
|
| 21 |
-
with pytest.warns(
|
| 22 |
-
UserWarning,
|
| 23 |
-
match="Tool.from_function\\(\\) is deprecated. Use FunctionTool.from_function\\(\\) instead.",
|
| 24 |
-
):
|
| 25 |
-
tool = Tool.from_function(example_function)
|
| 26 |
-
|
| 27 |
-
# Verify the tool was created correctly despite the warning
|
| 28 |
-
assert isinstance(tool, FunctionTool)
|
| 29 |
-
assert tool.name == "example_function"
|
| 30 |
-
assert tool.description == "Example function for testing."
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def test_tool_from_function_produces_same_result_as_function_tool():
|
| 34 |
-
"""Test that Tool.from_function() produces the same result as FunctionTool.from_function()."""
|
| 35 |
-
|
| 36 |
-
def example_function(x: int, y: str = "default") -> dict:
|
| 37 |
-
"""Example function with parameters."""
|
| 38 |
-
return {"x": x, "y": y}
|
| 39 |
-
|
| 40 |
-
# Create tool using the deprecated method (with warning suppressed)
|
| 41 |
-
with warnings.catch_warnings():
|
| 42 |
-
warnings.simplefilter("ignore")
|
| 43 |
-
deprecated_tool = Tool.from_function(example_function)
|
| 44 |
-
|
| 45 |
-
# Create tool using the new method
|
| 46 |
-
new_tool = FunctionTool.from_function(example_function)
|
| 47 |
-
|
| 48 |
-
# They should be equivalent
|
| 49 |
-
assert deprecated_tool == new_tool
|
| 50 |
-
assert deprecated_tool.name == new_tool.name
|
| 51 |
-
assert deprecated_tool.description == new_tool.description
|
| 52 |
-
assert deprecated_tool.parameters == new_tool.parameters
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def test_tool_from_function_with_overrides():
|
| 56 |
-
"""Test that Tool.from_function() works with parameter overrides."""
|
| 57 |
-
|
| 58 |
-
def example_function() -> str:
|
| 59 |
-
"""Original description."""
|
| 60 |
-
return "test"
|
| 61 |
-
|
| 62 |
-
custom_name = "custom_tool_name"
|
| 63 |
-
custom_description = "Custom description"
|
| 64 |
-
custom_tags = {"test", "deprecated"}
|
| 65 |
-
|
| 66 |
-
with pytest.warns(UserWarning, match="Tool.from_function\\(\\) is deprecated"):
|
| 67 |
-
tool = Tool.from_function(
|
| 68 |
-
example_function,
|
| 69 |
-
name=custom_name,
|
| 70 |
-
description=custom_description,
|
| 71 |
-
tags=custom_tags,
|
| 72 |
-
)
|
| 73 |
-
|
| 74 |
-
assert tool.name == custom_name
|
| 75 |
-
assert tool.description == custom_description
|
| 76 |
-
assert tool.tags == custom_tags
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/prompts/test_prompt_manager.py
CHANGED
|
@@ -5,7 +5,7 @@ import pytest
|
|
| 5 |
from fastmcp import Context
|
| 6 |
from fastmcp.exceptions import NotFoundError, PromptError
|
| 7 |
from fastmcp.prompts import Prompt
|
| 8 |
-
from fastmcp.prompts.prompt import PromptMessage, TextContent
|
| 9 |
from fastmcp.prompts.prompt_manager import PromptManager
|
| 10 |
|
| 11 |
|
|
@@ -97,6 +97,7 @@ class TestPromptManager:
|
|
| 97 |
# Should have replaced with the new prompt
|
| 98 |
prompt = manager.get_prompt("test_prompt")
|
| 99 |
assert prompt is not None
|
|
|
|
| 100 |
assert prompt.fn.__name__ == "replacement_fn"
|
| 101 |
|
| 102 |
def test_ignore_duplicate_prompts(self):
|
|
@@ -118,8 +119,10 @@ class TestPromptManager:
|
|
| 118 |
# Should keep the original
|
| 119 |
prompt = manager.get_prompt("test_prompt")
|
| 120 |
assert prompt is not None
|
|
|
|
| 121 |
assert prompt.fn.__name__ == "original_fn"
|
| 122 |
# Result should be the original prompt
|
|
|
|
| 123 |
assert result.fn.__name__ == "original_fn"
|
| 124 |
|
| 125 |
def test_get_prompts(self):
|
|
|
|
| 5 |
from fastmcp import Context
|
| 6 |
from fastmcp.exceptions import NotFoundError, PromptError
|
| 7 |
from fastmcp.prompts import Prompt
|
| 8 |
+
from fastmcp.prompts.prompt import FunctionPrompt, PromptMessage, TextContent
|
| 9 |
from fastmcp.prompts.prompt_manager import PromptManager
|
| 10 |
|
| 11 |
|
|
|
|
| 97 |
# Should have replaced with the new prompt
|
| 98 |
prompt = manager.get_prompt("test_prompt")
|
| 99 |
assert prompt is not None
|
| 100 |
+
assert isinstance(prompt, FunctionPrompt)
|
| 101 |
assert prompt.fn.__name__ == "replacement_fn"
|
| 102 |
|
| 103 |
def test_ignore_duplicate_prompts(self):
|
|
|
|
| 119 |
# Should keep the original
|
| 120 |
prompt = manager.get_prompt("test_prompt")
|
| 121 |
assert prompt is not None
|
| 122 |
+
assert isinstance(prompt, FunctionPrompt)
|
| 123 |
assert prompt.fn.__name__ == "original_fn"
|
| 124 |
# Result should be the original prompt
|
| 125 |
+
assert isinstance(result, FunctionPrompt)
|
| 126 |
assert result.fn.__name__ == "original_fn"
|
| 127 |
|
| 128 |
def test_get_prompts(self):
|
tests/tools/test_tool.py
CHANGED
|
@@ -5,7 +5,7 @@ from pydantic import AnyUrl, BaseModel
|
|
| 5 |
from fastmcp import FastMCP, Image
|
| 6 |
from fastmcp.client import Client
|
| 7 |
from fastmcp.exceptions import ToolError
|
| 8 |
-
from fastmcp.tools.tool import
|
| 9 |
from fastmcp.utilities.tests import temporary_settings
|
| 10 |
|
| 11 |
|
|
@@ -17,7 +17,7 @@ class TestToolFromFunction:
|
|
| 17 |
"""Add two numbers."""
|
| 18 |
return a + b
|
| 19 |
|
| 20 |
-
tool =
|
| 21 |
|
| 22 |
assert tool.name == "add"
|
| 23 |
assert tool.description == "Add two numbers."
|
|
@@ -32,7 +32,7 @@ class TestToolFromFunction:
|
|
| 32 |
"""Fetch data from URL."""
|
| 33 |
return f"Data from {url}"
|
| 34 |
|
| 35 |
-
tool =
|
| 36 |
|
| 37 |
assert tool.name == "fetch_data"
|
| 38 |
assert tool.description == "Fetch data from URL."
|
|
@@ -46,7 +46,7 @@ class TestToolFromFunction:
|
|
| 46 |
"""ignore this"""
|
| 47 |
return x + y
|
| 48 |
|
| 49 |
-
tool =
|
| 50 |
assert tool.name == "Adder"
|
| 51 |
assert tool.description == "Adds two numbers."
|
| 52 |
assert len(tool.parameters["properties"]) == 2
|
|
@@ -61,7 +61,7 @@ class TestToolFromFunction:
|
|
| 61 |
"""ignore this"""
|
| 62 |
return x + y
|
| 63 |
|
| 64 |
-
tool =
|
| 65 |
assert tool.name == "Adder"
|
| 66 |
assert tool.description == "Adds two numbers."
|
| 67 |
assert len(tool.parameters["properties"]) == 2
|
|
@@ -79,7 +79,7 @@ class TestToolFromFunction:
|
|
| 79 |
"""Create a new user."""
|
| 80 |
return {"id": 1, **user.model_dump()}
|
| 81 |
|
| 82 |
-
tool =
|
| 83 |
|
| 84 |
assert tool.name == "create_user"
|
| 85 |
assert tool.description == "Create a new user."
|
|
@@ -91,7 +91,7 @@ class TestToolFromFunction:
|
|
| 91 |
def image_tool(data: bytes) -> Image:
|
| 92 |
return Image(data=data)
|
| 93 |
|
| 94 |
-
tool =
|
| 95 |
|
| 96 |
result = await tool.run({"data": "test.png"})
|
| 97 |
assert tool.parameters["properties"]["data"]["type"] == "string"
|
|
@@ -99,24 +99,24 @@ class TestToolFromFunction:
|
|
| 99 |
|
| 100 |
def test_non_callable_fn(self):
|
| 101 |
with pytest.raises(TypeError, match="not a callable object"):
|
| 102 |
-
|
| 103 |
|
| 104 |
def test_lambda(self):
|
| 105 |
-
tool =
|
| 106 |
assert tool.name == "my_tool"
|
| 107 |
|
| 108 |
def test_lambda_with_no_name(self):
|
| 109 |
with pytest.raises(
|
| 110 |
ValueError, match="You must provide a name for lambda functions"
|
| 111 |
):
|
| 112 |
-
|
| 113 |
|
| 114 |
def test_private_arguments(self):
|
| 115 |
def add(_a: int, _b: int) -> int:
|
| 116 |
"""Add two numbers."""
|
| 117 |
return _a + _b
|
| 118 |
|
| 119 |
-
tool =
|
| 120 |
assert tool.parameters["properties"]["_a"]["type"] == "integer"
|
| 121 |
assert tool.parameters["properties"]["_b"]["type"] == "integer"
|
| 122 |
|
|
@@ -128,7 +128,7 @@ class TestToolFromFunction:
|
|
| 128 |
with pytest.raises(
|
| 129 |
ValueError, match=r"Functions with \*args are not supported as tools"
|
| 130 |
):
|
| 131 |
-
|
| 132 |
|
| 133 |
def test_tool_with_varkwargs_not_allowed(self):
|
| 134 |
def func(a: int, b: int, **kwargs: int) -> int:
|
|
@@ -138,7 +138,7 @@ class TestToolFromFunction:
|
|
| 138 |
with pytest.raises(
|
| 139 |
ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
|
| 140 |
):
|
| 141 |
-
|
| 142 |
|
| 143 |
async def test_instance_method(self):
|
| 144 |
class MyClass:
|
|
@@ -148,7 +148,7 @@ class TestToolFromFunction:
|
|
| 148 |
|
| 149 |
obj = MyClass()
|
| 150 |
|
| 151 |
-
tool =
|
| 152 |
assert tool.name == "add"
|
| 153 |
assert tool.description == "Add two numbers."
|
| 154 |
assert "self" not in tool.parameters["properties"]
|
|
@@ -164,7 +164,7 @@ class TestToolFromFunction:
|
|
| 164 |
with pytest.raises(
|
| 165 |
ValueError, match=r"Functions with \*args are not supported as tools"
|
| 166 |
):
|
| 167 |
-
|
| 168 |
|
| 169 |
async def test_instance_method_with_varkwargs_not_allowed(self):
|
| 170 |
class MyClass:
|
|
@@ -177,7 +177,7 @@ class TestToolFromFunction:
|
|
| 177 |
with pytest.raises(
|
| 178 |
ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
|
| 179 |
):
|
| 180 |
-
|
| 181 |
|
| 182 |
async def test_classmethod(self):
|
| 183 |
class MyClass:
|
|
@@ -188,7 +188,7 @@ class TestToolFromFunction:
|
|
| 188 |
"""Add two numbers."""
|
| 189 |
return x + y
|
| 190 |
|
| 191 |
-
tool =
|
| 192 |
assert tool.name == "call"
|
| 193 |
assert tool.description == "Add two numbers."
|
| 194 |
assert "x" in tool.parameters["properties"]
|
|
@@ -203,7 +203,7 @@ class TestToolFromFunction:
|
|
| 203 |
def process_list(items: list[int]) -> int:
|
| 204 |
return sum(items)
|
| 205 |
|
| 206 |
-
tool =
|
| 207 |
|
| 208 |
result = await tool.run(arguments={"items": [1, 2, 3, 4, 5]})
|
| 209 |
assert isinstance(result[0], TextContent)
|
|
@@ -225,7 +225,7 @@ class TestLegacyToolJsonParsing:
|
|
| 225 |
return f"{x}-{','.join(y)}"
|
| 226 |
|
| 227 |
# Create a tool to use its JSON pre-parsing logic
|
| 228 |
-
tool =
|
| 229 |
|
| 230 |
# Prepare arguments where some are JSON strings
|
| 231 |
json_args = {
|
|
@@ -243,7 +243,7 @@ class TestLegacyToolJsonParsing:
|
|
| 243 |
def func_with_str_types(str_or_list: str | list[str]) -> str | list[str]:
|
| 244 |
return str_or_list
|
| 245 |
|
| 246 |
-
tool =
|
| 247 |
|
| 248 |
# Test regular string input (should remain a string)
|
| 249 |
result = await tool.run({"str_or_list": "hello"})
|
|
@@ -269,7 +269,7 @@ class TestLegacyToolJsonParsing:
|
|
| 269 |
def func_with_str_types(string: str) -> str:
|
| 270 |
return string
|
| 271 |
|
| 272 |
-
tool =
|
| 273 |
|
| 274 |
# Invalid JSON should remain a string
|
| 275 |
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
|
@@ -284,7 +284,7 @@ class TestLegacyToolJsonParsing:
|
|
| 284 |
) -> str | dict[int, str] | None:
|
| 285 |
return string
|
| 286 |
|
| 287 |
-
tool =
|
| 288 |
|
| 289 |
# Invalid JSON for the union type should remain a string
|
| 290 |
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
|
@@ -301,7 +301,7 @@ class TestLegacyToolJsonParsing:
|
|
| 301 |
def func_with_complex_type(data: SomeModel) -> SomeModel:
|
| 302 |
return data
|
| 303 |
|
| 304 |
-
tool =
|
| 305 |
|
| 306 |
# Valid JSON for the model
|
| 307 |
valid_json = '{"x": 1, "y": {"1": "hello"}}'
|
|
|
|
| 5 |
from fastmcp import FastMCP, Image
|
| 6 |
from fastmcp.client import Client
|
| 7 |
from fastmcp.exceptions import ToolError
|
| 8 |
+
from fastmcp.tools.tool import Tool, _convert_to_content
|
| 9 |
from fastmcp.utilities.tests import temporary_settings
|
| 10 |
|
| 11 |
|
|
|
|
| 17 |
"""Add two numbers."""
|
| 18 |
return a + b
|
| 19 |
|
| 20 |
+
tool = Tool.from_function(add)
|
| 21 |
|
| 22 |
assert tool.name == "add"
|
| 23 |
assert tool.description == "Add two numbers."
|
|
|
|
| 32 |
"""Fetch data from URL."""
|
| 33 |
return f"Data from {url}"
|
| 34 |
|
| 35 |
+
tool = Tool.from_function(fetch_data)
|
| 36 |
|
| 37 |
assert tool.name == "fetch_data"
|
| 38 |
assert tool.description == "Fetch data from URL."
|
|
|
|
| 46 |
"""ignore this"""
|
| 47 |
return x + y
|
| 48 |
|
| 49 |
+
tool = Tool.from_function(Adder())
|
| 50 |
assert tool.name == "Adder"
|
| 51 |
assert tool.description == "Adds two numbers."
|
| 52 |
assert len(tool.parameters["properties"]) == 2
|
|
|
|
| 61 |
"""ignore this"""
|
| 62 |
return x + y
|
| 63 |
|
| 64 |
+
tool = Tool.from_function(Adder())
|
| 65 |
assert tool.name == "Adder"
|
| 66 |
assert tool.description == "Adds two numbers."
|
| 67 |
assert len(tool.parameters["properties"]) == 2
|
|
|
|
| 79 |
"""Create a new user."""
|
| 80 |
return {"id": 1, **user.model_dump()}
|
| 81 |
|
| 82 |
+
tool = Tool.from_function(create_user)
|
| 83 |
|
| 84 |
assert tool.name == "create_user"
|
| 85 |
assert tool.description == "Create a new user."
|
|
|
|
| 91 |
def image_tool(data: bytes) -> Image:
|
| 92 |
return Image(data=data)
|
| 93 |
|
| 94 |
+
tool = Tool.from_function(image_tool)
|
| 95 |
|
| 96 |
result = await tool.run({"data": "test.png"})
|
| 97 |
assert tool.parameters["properties"]["data"]["type"] == "string"
|
|
|
|
| 99 |
|
| 100 |
def test_non_callable_fn(self):
|
| 101 |
with pytest.raises(TypeError, match="not a callable object"):
|
| 102 |
+
Tool.from_function(1) # type: ignore
|
| 103 |
|
| 104 |
def test_lambda(self):
|
| 105 |
+
tool = Tool.from_function(lambda x: x, name="my_tool")
|
| 106 |
assert tool.name == "my_tool"
|
| 107 |
|
| 108 |
def test_lambda_with_no_name(self):
|
| 109 |
with pytest.raises(
|
| 110 |
ValueError, match="You must provide a name for lambda functions"
|
| 111 |
):
|
| 112 |
+
Tool.from_function(lambda x: x)
|
| 113 |
|
| 114 |
def test_private_arguments(self):
|
| 115 |
def add(_a: int, _b: int) -> int:
|
| 116 |
"""Add two numbers."""
|
| 117 |
return _a + _b
|
| 118 |
|
| 119 |
+
tool = Tool.from_function(add)
|
| 120 |
assert tool.parameters["properties"]["_a"]["type"] == "integer"
|
| 121 |
assert tool.parameters["properties"]["_b"]["type"] == "integer"
|
| 122 |
|
|
|
|
| 128 |
with pytest.raises(
|
| 129 |
ValueError, match=r"Functions with \*args are not supported as tools"
|
| 130 |
):
|
| 131 |
+
Tool.from_function(func)
|
| 132 |
|
| 133 |
def test_tool_with_varkwargs_not_allowed(self):
|
| 134 |
def func(a: int, b: int, **kwargs: int) -> int:
|
|
|
|
| 138 |
with pytest.raises(
|
| 139 |
ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
|
| 140 |
):
|
| 141 |
+
Tool.from_function(func)
|
| 142 |
|
| 143 |
async def test_instance_method(self):
|
| 144 |
class MyClass:
|
|
|
|
| 148 |
|
| 149 |
obj = MyClass()
|
| 150 |
|
| 151 |
+
tool = Tool.from_function(obj.add)
|
| 152 |
assert tool.name == "add"
|
| 153 |
assert tool.description == "Add two numbers."
|
| 154 |
assert "self" not in tool.parameters["properties"]
|
|
|
|
| 164 |
with pytest.raises(
|
| 165 |
ValueError, match=r"Functions with \*args are not supported as tools"
|
| 166 |
):
|
| 167 |
+
Tool.from_function(obj.add)
|
| 168 |
|
| 169 |
async def test_instance_method_with_varkwargs_not_allowed(self):
|
| 170 |
class MyClass:
|
|
|
|
| 177 |
with pytest.raises(
|
| 178 |
ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
|
| 179 |
):
|
| 180 |
+
Tool.from_function(obj.add)
|
| 181 |
|
| 182 |
async def test_classmethod(self):
|
| 183 |
class MyClass:
|
|
|
|
| 188 |
"""Add two numbers."""
|
| 189 |
return x + y
|
| 190 |
|
| 191 |
+
tool = Tool.from_function(MyClass.call)
|
| 192 |
assert tool.name == "call"
|
| 193 |
assert tool.description == "Add two numbers."
|
| 194 |
assert "x" in tool.parameters["properties"]
|
|
|
|
| 203 |
def process_list(items: list[int]) -> int:
|
| 204 |
return sum(items)
|
| 205 |
|
| 206 |
+
tool = Tool.from_function(process_list, serializer=custom_serializer)
|
| 207 |
|
| 208 |
result = await tool.run(arguments={"items": [1, 2, 3, 4, 5]})
|
| 209 |
assert isinstance(result[0], TextContent)
|
|
|
|
| 225 |
return f"{x}-{','.join(y)}"
|
| 226 |
|
| 227 |
# Create a tool to use its JSON pre-parsing logic
|
| 228 |
+
tool = Tool.from_function(simple_func)
|
| 229 |
|
| 230 |
# Prepare arguments where some are JSON strings
|
| 231 |
json_args = {
|
|
|
|
| 243 |
def func_with_str_types(str_or_list: str | list[str]) -> str | list[str]:
|
| 244 |
return str_or_list
|
| 245 |
|
| 246 |
+
tool = Tool.from_function(func_with_str_types)
|
| 247 |
|
| 248 |
# Test regular string input (should remain a string)
|
| 249 |
result = await tool.run({"str_or_list": "hello"})
|
|
|
|
| 269 |
def func_with_str_types(string: str) -> str:
|
| 270 |
return string
|
| 271 |
|
| 272 |
+
tool = Tool.from_function(func_with_str_types)
|
| 273 |
|
| 274 |
# Invalid JSON should remain a string
|
| 275 |
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
|
|
|
| 284 |
) -> str | dict[int, str] | None:
|
| 285 |
return string
|
| 286 |
|
| 287 |
+
tool = Tool.from_function(func_with_str_types)
|
| 288 |
|
| 289 |
# Invalid JSON for the union type should remain a string
|
| 290 |
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
|
|
|
| 301 |
def func_with_complex_type(data: SomeModel) -> SomeModel:
|
| 302 |
return data
|
| 303 |
|
| 304 |
+
tool = Tool.from_function(func_with_complex_type)
|
| 305 |
|
| 306 |
# Valid JSON for the model
|
| 307 |
valid_json = '{"x": 1, "y": {"1": "hello"}}'
|
tests/tools/test_tool_manager.py
CHANGED
|
@@ -568,7 +568,7 @@ class TestContextHandling:
|
|
| 568 |
|
| 569 |
def test_context_parameter_detection(self):
|
| 570 |
"""Test that context parameters are properly detected in
|
| 571 |
-
|
| 572 |
|
| 573 |
def tool_with_context(x: int, ctx: Context) -> str:
|
| 574 |
return str(x)
|
|
@@ -634,7 +634,7 @@ class TestContextHandling:
|
|
| 634 |
|
| 635 |
def test_parameterized_context_parameter_detection(self):
|
| 636 |
"""Test that context parameters are properly detected in
|
| 637 |
-
|
| 638 |
|
| 639 |
def tool_with_context(x: int, ctx: Context) -> str:
|
| 640 |
return str(x)
|
|
@@ -651,7 +651,7 @@ class TestContextHandling:
|
|
| 651 |
|
| 652 |
def test_parameterized_union_context_parameter_detection(self):
|
| 653 |
"""Test that context parameters are properly detected in
|
| 654 |
-
|
| 655 |
|
| 656 |
def tool_with_context(x: int, ctx: Context | None) -> str:
|
| 657 |
return str(x)
|
|
|
|
| 568 |
|
| 569 |
def test_context_parameter_detection(self):
|
| 570 |
"""Test that context parameters are properly detected in
|
| 571 |
+
Tool.from_function()."""
|
| 572 |
|
| 573 |
def tool_with_context(x: int, ctx: Context) -> str:
|
| 574 |
return str(x)
|
|
|
|
| 634 |
|
| 635 |
def test_parameterized_context_parameter_detection(self):
|
| 636 |
"""Test that context parameters are properly detected in
|
| 637 |
+
Tool.from_function()."""
|
| 638 |
|
| 639 |
def tool_with_context(x: int, ctx: Context) -> str:
|
| 640 |
return str(x)
|
|
|
|
| 651 |
|
| 652 |
def test_parameterized_union_context_parameter_detection(self):
|
| 653 |
"""Test that context parameters are properly detected in
|
| 654 |
+
Tool.from_function()."""
|
| 655 |
|
| 656 |
def tool_with_context(x: int, ctx: Context | None) -> str:
|
| 657 |
return str(x)
|