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.
@@ -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] = Field(exclude=True)
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
- sig = inspect.signature(fn)
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()
@@ -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.tools.base import Tool
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, warn_on_duplicate_tools: bool = True):
24
+ def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
24
25
  self._tools: dict[str, Tool] = {}
25
- self.warn_on_duplicate_tools = warn_on_duplicate_tools
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 add_tool(
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.warn_on_duplicate_tools:
51
+ if self.duplicate_behavior == DuplicateBehavior.WARN:
46
52
  logger.warning(f"Tool already exists: {tool.name}")
47
- return existing
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
- # Create a shallow copy of the tool with the prefixed name
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._tools[prefixed_name] = copied_tool
90
- logger.debug(f"Imported tool: {name} as {prefixed_name}")
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
- sig = _get_typed_signature(func)
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__", {})