fastmcp 2.0.0__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/cli/cli.py +4 -2
- fastmcp/client/client.py +80 -35
- fastmcp/client/transports.py +22 -0
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/{base.py → prompt.py} +29 -19
- fastmcp/prompts/prompt_manager.py +29 -12
- fastmcp/resources/__init__.py +3 -3
- fastmcp/resources/{base.py → resource.py} +20 -1
- fastmcp/resources/resource_manager.py +145 -19
- fastmcp/resources/{templates.py → template.py} +23 -3
- fastmcp/resources/types.py +2 -2
- fastmcp/server/context.py +1 -1
- fastmcp/server/openapi.py +16 -4
- fastmcp/server/proxy.py +31 -27
- fastmcp/server/server.py +247 -97
- fastmcp/settings.py +11 -3
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/{base.py → tool.py} +26 -4
- fastmcp/tools/tool_manager.py +22 -16
- fastmcp/utilities/decorators.py +101 -0
- fastmcp/utilities/func_metadata.py +4 -1
- fastmcp/utilities/openapi.py +671 -292
- fastmcp/utilities/types.py +12 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/METADATA +72 -52
- fastmcp-2.1.1.dist-info/RECORD +40 -0
- fastmcp-2.0.0.dist-info/RECORD +0 -39
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.0.0.dist-info → fastmcp-2.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,12 +2,14 @@ from __future__ import annotations as _annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import BaseModel, BeforeValidator, Field
|
|
8
|
+
from typing_extensions import Self
|
|
8
9
|
|
|
9
10
|
from fastmcp.exceptions import ToolError
|
|
10
11
|
from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
|
12
|
+
from fastmcp.utilities.types import _convert_set_defaults
|
|
11
13
|
|
|
12
14
|
if TYPE_CHECKING:
|
|
13
15
|
from mcp.server.session import ServerSessionT
|
|
@@ -19,7 +21,7 @@ if TYPE_CHECKING:
|
|
|
19
21
|
class Tool(BaseModel):
|
|
20
22
|
"""Internal tool registration info."""
|
|
21
23
|
|
|
22
|
-
fn: Callable[..., Any]
|
|
24
|
+
fn: Callable[..., Any]
|
|
23
25
|
name: str = Field(description="Name of the tool")
|
|
24
26
|
description: str = Field(description="Description of what the tool does")
|
|
25
27
|
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
|
|
@@ -31,6 +33,9 @@ class Tool(BaseModel):
|
|
|
31
33
|
context_kwarg: str | None = Field(
|
|
32
34
|
None, description="Name of the kwarg that should receive context"
|
|
33
35
|
)
|
|
36
|
+
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
37
|
+
default_factory=set, description="Tags for the tool"
|
|
38
|
+
)
|
|
34
39
|
|
|
35
40
|
@classmethod
|
|
36
41
|
def from_function(
|
|
@@ -39,6 +44,7 @@ class Tool(BaseModel):
|
|
|
39
44
|
name: str | None = None,
|
|
40
45
|
description: str | None = None,
|
|
41
46
|
context_kwarg: str | None = None,
|
|
47
|
+
tags: set[str] | None = None,
|
|
42
48
|
) -> Tool:
|
|
43
49
|
"""Create a Tool from a function."""
|
|
44
50
|
from fastmcp import Context
|
|
@@ -52,7 +58,10 @@ class Tool(BaseModel):
|
|
|
52
58
|
is_async = inspect.iscoroutinefunction(fn)
|
|
53
59
|
|
|
54
60
|
if context_kwarg is None:
|
|
55
|
-
|
|
61
|
+
if isinstance(fn, classmethod):
|
|
62
|
+
sig = inspect.signature(fn.__func__)
|
|
63
|
+
else:
|
|
64
|
+
sig = inspect.signature(fn)
|
|
56
65
|
for param_name, param in sig.parameters.items():
|
|
57
66
|
if param.annotation is Context:
|
|
58
67
|
context_kwarg = param_name
|
|
@@ -72,6 +81,7 @@ class Tool(BaseModel):
|
|
|
72
81
|
fn_metadata=func_arg_metadata,
|
|
73
82
|
is_async=is_async,
|
|
74
83
|
context_kwarg=context_kwarg,
|
|
84
|
+
tags=tags or set(),
|
|
75
85
|
)
|
|
76
86
|
|
|
77
87
|
async def run(
|
|
@@ -91,3 +101,15 @@ class Tool(BaseModel):
|
|
|
91
101
|
)
|
|
92
102
|
except Exception as e:
|
|
93
103
|
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
|
104
|
+
|
|
105
|
+
def copy(self, updates: dict[str, Any] | None = None) -> Self:
|
|
106
|
+
"""Copy the tool with optional updates."""
|
|
107
|
+
data = self.model_dump()
|
|
108
|
+
if updates:
|
|
109
|
+
data.update(updates)
|
|
110
|
+
return type(self)(**data)
|
|
111
|
+
|
|
112
|
+
def __eq__(self, other: object) -> bool:
|
|
113
|
+
if not isinstance(other, Tool):
|
|
114
|
+
return False
|
|
115
|
+
return self.model_dump() == other.model_dump()
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -6,7 +6,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
from mcp.shared.context import LifespanContextT
|
|
7
7
|
|
|
8
8
|
from fastmcp.exceptions import ToolError
|
|
9
|
-
from fastmcp.
|
|
9
|
+
from fastmcp.settings import DuplicateBehavior
|
|
10
|
+
from fastmcp.tools.tool import Tool
|
|
10
11
|
from fastmcp.utilities.logging import get_logger
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
@@ -20,9 +21,9 @@ logger = get_logger(__name__)
|
|
|
20
21
|
class ToolManager:
|
|
21
22
|
"""Manages FastMCP tools."""
|
|
22
23
|
|
|
23
|
-
def __init__(self,
|
|
24
|
+
def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
|
|
24
25
|
self._tools: dict[str, Tool] = {}
|
|
25
|
-
self.
|
|
26
|
+
self.duplicate_behavior = duplicate_behavior
|
|
26
27
|
|
|
27
28
|
def get_tool(self, name: str) -> Tool | None:
|
|
28
29
|
"""Get tool by name."""
|
|
@@ -32,19 +33,30 @@ class ToolManager:
|
|
|
32
33
|
"""List all registered tools."""
|
|
33
34
|
return list(self._tools.values())
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
+
def add_tool_from_fn(
|
|
36
37
|
self,
|
|
37
38
|
fn: Callable[..., Any],
|
|
38
39
|
name: str | None = None,
|
|
39
40
|
description: str | None = None,
|
|
41
|
+
tags: set[str] | None = None,
|
|
40
42
|
) -> Tool:
|
|
41
43
|
"""Add a tool to the server."""
|
|
42
|
-
tool = Tool.from_function(fn, name=name, description=description)
|
|
44
|
+
tool = Tool.from_function(fn, name=name, description=description, tags=tags)
|
|
45
|
+
return self.add_tool(tool)
|
|
46
|
+
|
|
47
|
+
def add_tool(self, tool: Tool) -> Tool:
|
|
48
|
+
"""Register a tool with the server."""
|
|
43
49
|
existing = self._tools.get(tool.name)
|
|
44
50
|
if existing:
|
|
45
|
-
if self.
|
|
51
|
+
if self.duplicate_behavior == DuplicateBehavior.WARN:
|
|
46
52
|
logger.warning(f"Tool already exists: {tool.name}")
|
|
47
|
-
|
|
53
|
+
self._tools[tool.name] = tool
|
|
54
|
+
elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
|
|
55
|
+
self._tools[tool.name] = tool
|
|
56
|
+
elif self.duplicate_behavior == DuplicateBehavior.ERROR:
|
|
57
|
+
raise ValueError(f"Tool already exists: {tool.name}")
|
|
58
|
+
elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
|
|
59
|
+
pass
|
|
48
60
|
self._tools[tool.name] = tool
|
|
49
61
|
return tool
|
|
50
62
|
|
|
@@ -78,13 +90,7 @@ class ToolManager:
|
|
|
78
90
|
for name, tool in tool_manager._tools.items():
|
|
79
91
|
prefixed_name = f"{prefix}{name}" if prefix else name
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
copied_tool = Tool.from_function(
|
|
83
|
-
tool.fn,
|
|
84
|
-
name=prefixed_name,
|
|
85
|
-
description=tool.description,
|
|
86
|
-
)
|
|
87
|
-
|
|
93
|
+
new_tool = tool.copy(updates=dict(name=prefixed_name))
|
|
88
94
|
# Store the copied tool
|
|
89
|
-
self.
|
|
90
|
-
logger.debug(f
|
|
95
|
+
self.add_tool(new_tool)
|
|
96
|
+
logger.debug(f'Imported tool "{name}" as "{prefixed_name}"')
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Generic, ParamSpec, TypeVar, cast, overload
|
|
4
|
+
|
|
5
|
+
from typing_extensions import Self
|
|
6
|
+
|
|
7
|
+
R = TypeVar("R")
|
|
8
|
+
P = ParamSpec("P")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DecoratedFunction(Generic[P, R]):
|
|
12
|
+
"""Descriptor for decorated functions.
|
|
13
|
+
|
|
14
|
+
You can return this object from a decorator to ensure that it works across
|
|
15
|
+
all types of functions: vanilla, instance methods, class methods, and static
|
|
16
|
+
methods; both synchronous and asynchronous.
|
|
17
|
+
|
|
18
|
+
This class is used to store the original function and metadata about how to
|
|
19
|
+
register it as a tool.
|
|
20
|
+
|
|
21
|
+
Example usage:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
def my_decorator(fn: Callable[P, R]) -> DecoratedFunction[P, R]:
|
|
25
|
+
return DecoratedFunction(fn)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
On a function:
|
|
29
|
+
```python
|
|
30
|
+
@my_decorator
|
|
31
|
+
def my_function(a: int, b: int) -> int:
|
|
32
|
+
return a + b
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
On an instance method:
|
|
36
|
+
```python
|
|
37
|
+
class Test:
|
|
38
|
+
@my_decorator
|
|
39
|
+
def my_function(self, a: int, b: int) -> int:
|
|
40
|
+
return a + b
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
On a class method:
|
|
44
|
+
```python
|
|
45
|
+
class Test:
|
|
46
|
+
@classmethod
|
|
47
|
+
@my_decorator
|
|
48
|
+
def my_function(cls, a: int, b: int) -> int:
|
|
49
|
+
return a + b
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Note that for classmethods, the decorator must be applied first, then
|
|
53
|
+
`@classmethod` on top.
|
|
54
|
+
|
|
55
|
+
On a static method:
|
|
56
|
+
```python
|
|
57
|
+
class Test:
|
|
58
|
+
@staticmethod
|
|
59
|
+
@my_decorator
|
|
60
|
+
def my_function(a: int, b: int) -> int:
|
|
61
|
+
return a + b
|
|
62
|
+
```
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, fn: Callable[P, R]):
|
|
66
|
+
self.fn = fn
|
|
67
|
+
|
|
68
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
69
|
+
"""Call the original function."""
|
|
70
|
+
try:
|
|
71
|
+
return self.fn(*args, **kwargs)
|
|
72
|
+
except TypeError as e:
|
|
73
|
+
if "'classmethod' object is not callable" in str(e):
|
|
74
|
+
raise TypeError(
|
|
75
|
+
"To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top."
|
|
76
|
+
)
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
@overload
|
|
80
|
+
def __get__(self, instance: None, owner: type | None = None) -> Self: ...
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
def __get__(
|
|
84
|
+
self, instance: object, owner: type | None = None
|
|
85
|
+
) -> Callable[P, R]: ...
|
|
86
|
+
|
|
87
|
+
def __get__(
|
|
88
|
+
self, instance: object | None, owner: type | None = None
|
|
89
|
+
) -> Self | Callable[P, R]:
|
|
90
|
+
"""Return the original function when accessed from an instance, or self when accessed from the class."""
|
|
91
|
+
if instance is None:
|
|
92
|
+
return self
|
|
93
|
+
# Return the original function bound to the instance
|
|
94
|
+
return cast(Callable[P, R], self.fn.__get__(instance, owner))
|
|
95
|
+
|
|
96
|
+
def __repr__(self) -> str:
|
|
97
|
+
"""Return a representation that matches Python's function representation."""
|
|
98
|
+
module = getattr(self.fn, "__module__", "unknown")
|
|
99
|
+
qualname = getattr(self.fn, "__qualname__", str(self.fn))
|
|
100
|
+
sig_str = str(inspect.signature(self.fn))
|
|
101
|
+
return f"<function {module}.{qualname}{sig_str}>"
|
|
@@ -125,7 +125,10 @@ def func_metadata(
|
|
|
125
125
|
Returns:
|
|
126
126
|
A pydantic model representing the function's signature.
|
|
127
127
|
"""
|
|
128
|
-
|
|
128
|
+
if isinstance(func, classmethod):
|
|
129
|
+
sig = _get_typed_signature(func.__func__)
|
|
130
|
+
else:
|
|
131
|
+
sig = _get_typed_signature(func)
|
|
129
132
|
params = sig.parameters
|
|
130
133
|
dynamic_pydantic_model_params: dict[str, Any] = {}
|
|
131
134
|
globalns = getattr(func, "__globals__", {})
|