fastmcp 0.4.1__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/__init__.py +15 -4
- fastmcp/cli/__init__.py +0 -1
- fastmcp/cli/claude.py +13 -11
- fastmcp/cli/cli.py +61 -41
- fastmcp/client/__init__.py +25 -0
- fastmcp/client/base.py +1 -0
- fastmcp/client/client.py +181 -0
- fastmcp/client/roots.py +75 -0
- fastmcp/client/sampling.py +50 -0
- fastmcp/client/transports.py +411 -0
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/base.py +27 -26
- fastmcp/prompts/prompt_manager.py +50 -12
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/base.py +2 -2
- fastmcp/resources/resource_manager.py +66 -9
- fastmcp/resources/templates.py +15 -10
- fastmcp/resources/types.py +16 -11
- fastmcp/server/__init__.py +5 -0
- fastmcp/server/context.py +222 -0
- fastmcp/server/openapi.py +625 -0
- fastmcp/server/proxy.py +219 -0
- fastmcp/{server.py → server/server.py} +251 -265
- fastmcp/settings.py +73 -0
- fastmcp/tools/base.py +28 -18
- fastmcp/tools/tool_manager.py +45 -10
- fastmcp/utilities/func_metadata.py +33 -19
- fastmcp/utilities/openapi.py +797 -0
- fastmcp/utilities/types.py +3 -4
- fastmcp-2.0.0.dist-info/METADATA +770 -0
- fastmcp-2.0.0.dist-info/RECORD +39 -0
- {fastmcp-0.4.1.dist-info → fastmcp-2.0.0.dist-info}/WHEEL +1 -1
- fastmcp-2.0.0.dist-info/licenses/LICENSE +201 -0
- fastmcp/prompts/manager.py +0 -50
- fastmcp-0.4.1.dist-info/METADATA +0 -587
- fastmcp-0.4.1.dist-info/RECORD +0 -28
- fastmcp-0.4.1.dist-info/licenses/LICENSE +0 -21
- {fastmcp-0.4.1.dist-info → fastmcp-2.0.0.dist-info}/entry_points.txt +0 -0
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
|
|
2
|
-
from fastmcp.exceptions import ToolError
|
|
1
|
+
from __future__ import annotations as _annotations
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
from
|
|
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
|
|
9
|
-
from
|
|
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
|
|
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:
|
|
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:
|
|
35
|
-
description:
|
|
36
|
-
context_kwarg:
|
|
37
|
-
) ->
|
|
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
|
|
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(
|
|
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(
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
from
|
|
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
|
|
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:
|
|
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) ->
|
|
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:
|
|
35
|
-
description:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
from
|
|
11
|
-
from pydantic._internal._typing_extra import
|
|
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,
|
|
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,
|
|
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(
|
|
109
|
-
|
|
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
|
|
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__}
|
|
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:
|
|
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 =
|
|
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
|
|