fastmcp 1.0__py3-none-any.whl → 2.0.0__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/settings.py ADDED
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal
4
+
5
+ from pydantic import Field
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+ if TYPE_CHECKING:
9
+ pass
10
+
11
+ LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
12
+
13
+
14
+ class Settings(BaseSettings):
15
+ """FastMCP settings."""
16
+
17
+ model_config = SettingsConfigDict(
18
+ env_prefix="FASTMCP_",
19
+ env_file=".env",
20
+ extra="ignore",
21
+ )
22
+
23
+ test_mode: bool = False
24
+ log_level: LOG_LEVEL = "INFO"
25
+
26
+
27
+ class ServerSettings(BaseSettings):
28
+ """FastMCP server settings.
29
+
30
+ All settings can be configured via environment variables with the prefix FASTMCP_.
31
+ For example, FASTMCP_DEBUG=true will set debug=True.
32
+ """
33
+
34
+ model_config = SettingsConfigDict(
35
+ env_prefix="FASTMCP_SERVER_",
36
+ env_file=".env",
37
+ extra="ignore",
38
+ )
39
+
40
+ log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
41
+
42
+ # HTTP settings
43
+ host: str = "0.0.0.0"
44
+ port: int = 8000
45
+ sse_path: str = "/sse"
46
+ message_path: str = "/messages/"
47
+ debug: bool = False
48
+
49
+ # resource settings
50
+ warn_on_duplicate_resources: bool = True
51
+
52
+ # tool settings
53
+ warn_on_duplicate_tools: bool = True
54
+
55
+ # prompt settings
56
+ warn_on_duplicate_prompts: bool = True
57
+
58
+ dependencies: list[str] = Field(
59
+ default_factory=list,
60
+ description="List of dependencies to install in the server environment",
61
+ )
62
+
63
+
64
+ class ClientSettings(BaseSettings):
65
+ """FastMCP client settings."""
66
+
67
+ model_config = SettingsConfigDict(
68
+ env_prefix="FASTMCP_CLIENT_",
69
+ env_file=".env",
70
+ extra="ignore",
71
+ )
72
+
73
+ log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
fastmcp/tools/base.py CHANGED
@@ -1,41 +1,48 @@
1
- import fastmcp
2
- from fastmcp.exceptions import ToolError
1
+ from __future__ import annotations as _annotations
3
2
 
4
- from fastmcp.utilities.func_metadata import func_metadata, FuncMetadata
5
- from pydantic import BaseModel, Field
3
+ import inspect
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
+ from pydantic import BaseModel, Field
7
8
 
8
- import inspect
9
- from typing import TYPE_CHECKING, Any, Callable, Optional
9
+ from fastmcp.exceptions import ToolError
10
+ from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
10
11
 
11
12
  if TYPE_CHECKING:
13
+ from mcp.server.session import ServerSessionT
14
+ from mcp.shared.context import LifespanContextT
15
+
12
16
  from fastmcp.server import Context
13
17
 
14
18
 
15
19
  class Tool(BaseModel):
16
20
  """Internal tool registration info."""
17
21
 
18
- fn: Callable = Field(exclude=True)
22
+ fn: Callable[..., Any] = Field(exclude=True)
19
23
  name: str = Field(description="Name of the tool")
20
24
  description: str = Field(description="Description of what the tool does")
21
- parameters: dict = Field(description="JSON schema for tool parameters")
25
+ parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
22
26
  fn_metadata: FuncMetadata = Field(
23
- description="Metadata about the function including a pydantic model for tool arguments"
27
+ description="Metadata about the function including a pydantic model for tool"
28
+ " arguments"
24
29
  )
25
30
  is_async: bool = Field(description="Whether the tool is async")
26
- context_kwarg: Optional[str] = Field(
31
+ context_kwarg: str | None = Field(
27
32
  None, description="Name of the kwarg that should receive context"
28
33
  )
29
34
 
30
35
  @classmethod
31
36
  def from_function(
32
37
  cls,
33
- fn: Callable,
34
- name: Optional[str] = None,
35
- description: Optional[str] = None,
36
- context_kwarg: Optional[str] = None,
37
- ) -> "Tool":
38
+ fn: Callable[..., Any],
39
+ name: str | None = None,
40
+ description: str | None = None,
41
+ context_kwarg: str | None = None,
42
+ ) -> Tool:
38
43
  """Create a Tool from a function."""
44
+ from fastmcp import Context
45
+
39
46
  func_name = name or fn.__name__
40
47
 
41
48
  if func_name == "<lambda>":
@@ -44,11 +51,10 @@ class Tool(BaseModel):
44
51
  func_doc = description or fn.__doc__ or ""
45
52
  is_async = inspect.iscoroutinefunction(fn)
46
53
 
47
- # Find context parameter if it exists
48
54
  if context_kwarg is None:
49
55
  sig = inspect.signature(fn)
50
56
  for param_name, param in sig.parameters.items():
51
- if param.annotation is fastmcp.Context:
57
+ if param.annotation is Context:
52
58
  context_kwarg = param_name
53
59
  break
54
60
 
@@ -68,7 +74,11 @@ class Tool(BaseModel):
68
74
  context_kwarg=context_kwarg,
69
75
  )
70
76
 
71
- async def run(self, arguments: dict, context: Optional["Context"] = None) -> Any:
77
+ async def run(
78
+ self,
79
+ arguments: dict[str, Any],
80
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
81
+ ) -> Any:
72
82
  """Run the tool with arguments."""
73
83
  try:
74
84
  return await self.fn_metadata.call_fn_with_arg_validation(
@@ -1,13 +1,17 @@
1
- from fastmcp.exceptions import ToolError
2
-
3
- from fastmcp.tools.base import Tool
1
+ from __future__ import annotations as _annotations
4
2
 
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
- from typing import Any, Callable, Dict, Optional, TYPE_CHECKING
6
+ from mcp.shared.context import LifespanContextT
7
7
 
8
+ from fastmcp.exceptions import ToolError
9
+ from fastmcp.tools.base import Tool
8
10
  from fastmcp.utilities.logging import get_logger
9
11
 
10
12
  if TYPE_CHECKING:
13
+ from mcp.server.session import ServerSessionT
14
+
11
15
  from fastmcp.server import Context
12
16
 
13
17
  logger = get_logger(__name__)
@@ -17,10 +21,10 @@ class ToolManager:
17
21
  """Manages FastMCP tools."""
18
22
 
19
23
  def __init__(self, warn_on_duplicate_tools: bool = True):
20
- self._tools: Dict[str, Tool] = {}
24
+ self._tools: dict[str, Tool] = {}
21
25
  self.warn_on_duplicate_tools = warn_on_duplicate_tools
22
26
 
23
- def get_tool(self, name: str) -> Optional[Tool]:
27
+ def get_tool(self, name: str) -> Tool | None:
24
28
  """Get tool by name."""
25
29
  return self._tools.get(name)
26
30
 
@@ -30,9 +34,9 @@ class ToolManager:
30
34
 
31
35
  def add_tool(
32
36
  self,
33
- fn: Callable,
34
- name: Optional[str] = None,
35
- description: Optional[str] = None,
37
+ fn: Callable[..., Any],
38
+ name: str | None = None,
39
+ description: str | None = None,
36
40
  ) -> Tool:
37
41
  """Add a tool to the server."""
38
42
  tool = Tool.from_function(fn, name=name, description=description)
@@ -45,7 +49,10 @@ class ToolManager:
45
49
  return tool
46
50
 
47
51
  async def call_tool(
48
- self, name: str, arguments: dict, context: Optional["Context"] = None
52
+ self,
53
+ name: str,
54
+ arguments: dict[str, Any],
55
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
49
56
  ) -> Any:
50
57
  """Call a tool by name with arguments."""
51
58
  tool = self.get_tool(name)
@@ -53,3 +60,31 @@ class ToolManager:
53
60
  raise ToolError(f"Unknown tool: {name}")
54
61
 
55
62
  return await tool.run(arguments, context=context)
63
+
64
+ def import_tools(
65
+ self, tool_manager: ToolManager, prefix: str | None = None
66
+ ) -> None:
67
+ """
68
+ Import all tools from another ToolManager with prefixed names.
69
+
70
+ Args:
71
+ tool_manager: Another ToolManager instance to import tools from
72
+ prefix: Prefix to add to tool names, including the delimiter.
73
+ The resulting tool name will be in the format "{prefix}{original_name}"
74
+ if prefix is provided, otherwise the original name is used.
75
+ For example, with prefix "weather/" and tool "forecast",
76
+ the imported tool would be available as "weather/forecast"
77
+ """
78
+ for name, tool in tool_manager._tools.items():
79
+ prefixed_name = f"{prefix}{name}" if prefix else name
80
+
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
+
88
+ # Store the copied tool
89
+ self._tools[prefixed_name] = copied_tool
90
+ logger.debug(f"Imported tool: {name} as {prefixed_name}")
@@ -1,22 +1,19 @@
1
1
  import inspect
2
- from collections.abc import Callable, Sequence, Awaitable
2
+ import json
3
+ from collections.abc import Awaitable, Callable, Sequence
3
4
  from typing import (
4
5
  Annotated,
5
6
  Any,
6
- Dict,
7
7
  ForwardRef,
8
8
  )
9
- from pydantic import Field
10
- from fastmcp.exceptions import InvalidSignature
11
- from pydantic._internal._typing_extra import eval_type_lenient
12
- import json
13
- from pydantic import BaseModel
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model
11
+ from pydantic._internal._typing_extra import eval_type_backport
14
12
  from pydantic.fields import FieldInfo
15
- from pydantic import ConfigDict, create_model
16
- from pydantic import WithJsonSchema
17
13
  from pydantic_core import PydanticUndefined
18
- from fastmcp.utilities.logging import get_logger
19
14
 
15
+ from fastmcp.exceptions import InvalidSignature
16
+ from fastmcp.utilities.logging import get_logger
20
17
 
21
18
  logger = get_logger(__name__)
22
19
 
@@ -30,7 +27,7 @@ class ArgModelBase(BaseModel):
30
27
  That is, sub-models etc are not dumped - they are kept as pydantic models.
31
28
  """
32
29
  kwargs: dict[str, Any] = {}
33
- for field_name in self.model_fields.keys():
30
+ for field_name in self.__class__.model_fields.keys():
34
31
  kwargs[field_name] = getattr(self, field_name)
35
32
  return kwargs
36
33
 
@@ -83,7 +80,7 @@ class FuncMetadata(BaseModel):
83
80
  dicts (JSON objects) as JSON strings, which can be pre-parsed here.
84
81
  """
85
82
  new_data = data.copy() # Shallow copy
86
- for field_name, field_info in self.arg_model.model_fields.items():
83
+ for field_name, _field_info in self.arg_model.model_fields.items():
87
84
  if field_name not in data.keys():
88
85
  continue
89
86
  if isinstance(data[field_name], str):
@@ -91,7 +88,7 @@ class FuncMetadata(BaseModel):
91
88
  pre_parsed = json.loads(data[field_name])
92
89
  except json.JSONDecodeError:
93
90
  continue # Not JSON - skip
94
- if isinstance(pre_parsed, (str, int, float)):
91
+ if isinstance(pre_parsed, str | int | float):
95
92
  # This is likely that the raw value is e.g. `"hello"` which we
96
93
  # Should really be parsed as '"hello"' in Python - but if we parse
97
94
  # it as JSON it'll turn into just 'hello'. So we skip it.
@@ -105,8 +102,11 @@ class FuncMetadata(BaseModel):
105
102
  )
106
103
 
107
104
 
108
- def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata:
109
- """Given a function, return metadata including a pydantic model representing its signature.
105
+ def func_metadata(
106
+ func: Callable[..., Any], skip_names: Sequence[str] = ()
107
+ ) -> FuncMetadata:
108
+ """Given a function, return metadata including a pydantic model representing its
109
+ signature.
110
110
 
111
111
  The use case for this is
112
112
  ```
@@ -115,7 +115,8 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat
115
115
  return func(**validated_args.model_dump_one_level())
116
116
  ```
117
117
 
118
- **critically** it also provides pre-parse helper to attempt to parse things from JSON.
118
+ **critically** it also provides pre-parse helper to attempt to parse things from
119
+ JSON.
119
120
 
120
121
  Args:
121
122
  func: The function to convert to a pydantic model
@@ -131,7 +132,7 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat
131
132
  for param in params.values():
132
133
  if param.name.startswith("_"):
133
134
  raise InvalidSignature(
134
- f"Parameter {param.name} of {func.__name__} may not start with an underscore"
135
+ f"Parameter {param.name} of {func.__name__} cannot start with '_'"
135
136
  )
136
137
  if param.name in skip_names:
137
138
  continue
@@ -175,10 +176,23 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat
175
176
  return resp
176
177
 
177
178
 
178
- def _get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
179
+ def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
180
+ def try_eval_type(
181
+ value: Any, globalns: dict[str, Any], localns: dict[str, Any]
182
+ ) -> tuple[Any, bool]:
183
+ try:
184
+ return eval_type_backport(value, globalns, localns), True
185
+ except NameError:
186
+ return value, False
187
+
179
188
  if isinstance(annotation, str):
180
189
  annotation = ForwardRef(annotation)
181
- annotation = eval_type_lenient(annotation, globalns, globalns)
190
+ annotation, status = try_eval_type(annotation, globalns, globalns)
191
+
192
+ # This check and raise could perhaps be skipped, and we (FastMCP) just call
193
+ # model_rebuild right before using it 🤷
194
+ if status is False:
195
+ raise InvalidSignature(f"Unable to evaluate type annotation {annotation}")
182
196
 
183
197
  return annotation
184
198