fastmcp 0.3.3__py3-none-any.whl → 0.3.5__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/claude.py +19 -2
- fastmcp/cli/cli.py +47 -6
- fastmcp/exceptions.py +4 -0
- fastmcp/resources/base.py +12 -19
- fastmcp/tools/base.py +19 -15
- fastmcp/utilities/func_metadata.py +200 -0
- {fastmcp-0.3.3.dist-info → fastmcp-0.3.5.dist-info}/METADATA +37 -16
- {fastmcp-0.3.3.dist-info → fastmcp-0.3.5.dist-info}/RECORD +11 -10
- {fastmcp-0.3.3.dist-info → fastmcp-0.3.5.dist-info}/WHEEL +0 -0
- {fastmcp-0.3.3.dist-info → fastmcp-0.3.5.dist-info}/entry_points.txt +0 -0
- {fastmcp-0.3.3.dist-info → fastmcp-0.3.5.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/claude.py
CHANGED
|
@@ -41,14 +41,31 @@ def update_claude_config(
|
|
|
41
41
|
with_packages: Optional list of additional packages to install
|
|
42
42
|
env_vars: Optional dictionary of environment variables. These are merged with
|
|
43
43
|
any existing variables, with new values taking precedence.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
RuntimeError: If Claude Desktop's config directory is not found, indicating
|
|
47
|
+
Claude Desktop may not be installed or properly set up.
|
|
44
48
|
"""
|
|
45
49
|
config_dir = get_claude_config_path()
|
|
46
50
|
if not config_dir:
|
|
47
|
-
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"Claude Desktop config directory not found. Please ensure Claude Desktop "
|
|
53
|
+
"is installed and has been run at least once to initialize its configuration."
|
|
54
|
+
)
|
|
48
55
|
|
|
49
56
|
config_file = config_dir / "claude_desktop_config.json"
|
|
50
57
|
if not config_file.exists():
|
|
51
|
-
|
|
58
|
+
try:
|
|
59
|
+
config_file.write_text("{}")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(
|
|
62
|
+
"Failed to create Claude config file",
|
|
63
|
+
extra={
|
|
64
|
+
"error": str(e),
|
|
65
|
+
"config_file": str(config_file),
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
return False
|
|
52
69
|
|
|
53
70
|
try:
|
|
54
71
|
config = json.loads(config_file.read_text())
|
fastmcp/cli/cli.py
CHANGED
|
@@ -11,8 +11,8 @@ import typer
|
|
|
11
11
|
from typing_extensions import Annotated
|
|
12
12
|
import dotenv
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
from . import
|
|
14
|
+
from fastmcp.cli import claude
|
|
15
|
+
from fastmcp.utilities.logging import get_logger
|
|
16
16
|
|
|
17
17
|
logger = get_logger("cli")
|
|
18
18
|
|
|
@@ -24,6 +24,22 @@ app = typer.Typer(
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _get_npx_command():
|
|
28
|
+
"""Get the correct npx command for the current platform."""
|
|
29
|
+
if sys.platform == "win32":
|
|
30
|
+
# Try both npx.cmd and npx.exe on Windows
|
|
31
|
+
for cmd in ["npx.cmd", "npx.exe", "npx"]:
|
|
32
|
+
try:
|
|
33
|
+
subprocess.run(
|
|
34
|
+
[cmd, "--version"], check=True, capture_output=True, shell=True
|
|
35
|
+
)
|
|
36
|
+
return cmd
|
|
37
|
+
except subprocess.CalledProcessError:
|
|
38
|
+
continue
|
|
39
|
+
return None
|
|
40
|
+
return "npx" # On Unix-like systems, just use npx
|
|
41
|
+
|
|
42
|
+
|
|
27
43
|
def _parse_env_var(env_var: str) -> Tuple[str, str]:
|
|
28
44
|
"""Parse environment variable string in format KEY=VALUE."""
|
|
29
45
|
if "=" not in env_var:
|
|
@@ -67,11 +83,17 @@ def _parse_file_path(file_spec: str) -> Tuple[Path, Optional[str]]:
|
|
|
67
83
|
Returns:
|
|
68
84
|
Tuple of (file_path, server_object)
|
|
69
85
|
"""
|
|
70
|
-
if
|
|
86
|
+
# First check if we have a Windows path (e.g., C:\...)
|
|
87
|
+
has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":"
|
|
88
|
+
|
|
89
|
+
# Split on the last colon, but only if it's not part of the Windows drive letter
|
|
90
|
+
# and there's actually another colon in the string after the drive letter
|
|
91
|
+
if ":" in (file_spec[2:] if has_windows_drive else file_spec):
|
|
71
92
|
file_str, server_object = file_spec.rsplit(":", 1)
|
|
72
93
|
else:
|
|
73
94
|
file_str, server_object = file_spec, None
|
|
74
95
|
|
|
96
|
+
# Resolve the file path
|
|
75
97
|
file_path = Path(file_str).expanduser().resolve()
|
|
76
98
|
if not file_path.exists():
|
|
77
99
|
logger.error(f"File not found: {file_path}")
|
|
@@ -93,6 +115,11 @@ def _import_server(file: Path, server_object: Optional[str] = None):
|
|
|
93
115
|
Returns:
|
|
94
116
|
The server object
|
|
95
117
|
"""
|
|
118
|
+
# Add parent directory to Python path so imports can be resolved
|
|
119
|
+
file_dir = str(file.parent)
|
|
120
|
+
if file_dir not in sys.path:
|
|
121
|
+
sys.path.insert(0, file_dir)
|
|
122
|
+
|
|
96
123
|
# Import the module
|
|
97
124
|
spec = importlib.util.spec_from_file_location("server_module", file)
|
|
98
125
|
if not spec or not spec.loader:
|
|
@@ -199,10 +226,22 @@ def dev(
|
|
|
199
226
|
with_packages = list(set(with_packages + server.dependencies))
|
|
200
227
|
|
|
201
228
|
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
|
202
|
-
|
|
229
|
+
|
|
230
|
+
# Get the correct npx command
|
|
231
|
+
npx_cmd = _get_npx_command()
|
|
232
|
+
if not npx_cmd:
|
|
233
|
+
logger.error(
|
|
234
|
+
"npx not found. Please ensure Node.js and npm are properly installed "
|
|
235
|
+
"and added to your system PATH."
|
|
236
|
+
)
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
# Run the MCP Inspector command with shell=True on Windows
|
|
240
|
+
shell = sys.platform == "win32"
|
|
203
241
|
process = subprocess.run(
|
|
204
|
-
[
|
|
242
|
+
[npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
|
|
205
243
|
check=True,
|
|
244
|
+
shell=shell,
|
|
206
245
|
)
|
|
207
246
|
sys.exit(process.returncode)
|
|
208
247
|
except subprocess.CalledProcessError as e:
|
|
@@ -217,7 +256,9 @@ def dev(
|
|
|
217
256
|
sys.exit(e.returncode)
|
|
218
257
|
except FileNotFoundError:
|
|
219
258
|
logger.error(
|
|
220
|
-
"npx not found. Please
|
|
259
|
+
"npx not found. Please ensure Node.js and npm are properly installed "
|
|
260
|
+
"and added to your system PATH. You may need to restart your terminal "
|
|
261
|
+
"after installation.",
|
|
221
262
|
extra={"file": str(file)},
|
|
222
263
|
)
|
|
223
264
|
sys.exit(1)
|
fastmcp/exceptions.py
CHANGED
fastmcp/resources/base.py
CHANGED
|
@@ -1,34 +1,17 @@
|
|
|
1
1
|
"""Base classes and interfaces for FastMCP resources."""
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Union
|
|
5
5
|
|
|
6
6
|
from pydantic import (
|
|
7
7
|
AnyUrl,
|
|
8
8
|
BaseModel,
|
|
9
|
-
BeforeValidator,
|
|
10
9
|
ConfigDict,
|
|
11
10
|
Field,
|
|
12
11
|
FileUrl,
|
|
13
12
|
ValidationInfo,
|
|
14
13
|
field_validator,
|
|
15
14
|
)
|
|
16
|
-
from pydantic.networks import _BaseUrl # TODO: remove this once pydantic is updated
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def maybe_cast_str_to_any_url(x) -> AnyUrl:
|
|
20
|
-
if isinstance(x, FileUrl):
|
|
21
|
-
return x
|
|
22
|
-
elif isinstance(x, AnyUrl):
|
|
23
|
-
return x
|
|
24
|
-
elif isinstance(x, str):
|
|
25
|
-
if x.startswith("file://"):
|
|
26
|
-
return FileUrl(x)
|
|
27
|
-
return AnyUrl(x)
|
|
28
|
-
raise ValueError(f"Expected str or AnyUrl, got {type(x)}")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
LaxAnyUrl = Annotated[_BaseUrl | str, BeforeValidator(maybe_cast_str_to_any_url)]
|
|
32
15
|
|
|
33
16
|
|
|
34
17
|
class Resource(BaseModel, abc.ABC):
|
|
@@ -36,7 +19,8 @@ class Resource(BaseModel, abc.ABC):
|
|
|
36
19
|
|
|
37
20
|
model_config = ConfigDict(validate_default=True)
|
|
38
21
|
|
|
39
|
-
uri:
|
|
22
|
+
# uri: Annotated[AnyUrl, BeforeValidator(maybe_cast_str_to_any_url)] = Field(
|
|
23
|
+
uri: AnyUrl = Field(default=..., description="URI of the resource")
|
|
40
24
|
name: str | None = Field(description="Name of the resource", default=None)
|
|
41
25
|
description: str | None = Field(
|
|
42
26
|
description="Description of the resource", default=None
|
|
@@ -47,6 +31,15 @@ class Resource(BaseModel, abc.ABC):
|
|
|
47
31
|
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
|
48
32
|
)
|
|
49
33
|
|
|
34
|
+
@field_validator("uri", mode="before")
|
|
35
|
+
def validate_uri(cls, uri: AnyUrl | str) -> AnyUrl:
|
|
36
|
+
if isinstance(uri, str):
|
|
37
|
+
# AnyUrl doesn't support triple-slashes, but files do ("file:///absolute/path")
|
|
38
|
+
if uri.startswith("file://"):
|
|
39
|
+
return FileUrl(uri)
|
|
40
|
+
return AnyUrl(uri)
|
|
41
|
+
return uri
|
|
42
|
+
|
|
50
43
|
@field_validator("name", mode="before")
|
|
51
44
|
@classmethod
|
|
52
45
|
def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
|
fastmcp/tools/base.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fastmcp
|
|
2
2
|
from fastmcp.exceptions import ToolError
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
4
|
+
from fastmcp.utilities.func_metadata import func_metadata, FuncMetadata
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
import inspect
|
|
@@ -19,6 +19,9 @@ class Tool(BaseModel):
|
|
|
19
19
|
name: str = Field(description="Name of the tool")
|
|
20
20
|
description: str = Field(description="Description of what the tool does")
|
|
21
21
|
parameters: dict = Field(description="JSON schema for tool parameters")
|
|
22
|
+
fn_metadata: FuncMetadata = Field(
|
|
23
|
+
description="Metadata about the function including a pydantic model for tool arguments"
|
|
24
|
+
)
|
|
22
25
|
is_async: bool = Field(description="Whether the tool is async")
|
|
23
26
|
context_kwarg: Optional[str] = Field(
|
|
24
27
|
None, description="Name of the kwarg that should receive context"
|
|
@@ -41,9 +44,6 @@ class Tool(BaseModel):
|
|
|
41
44
|
func_doc = description or fn.__doc__ or ""
|
|
42
45
|
is_async = inspect.iscoroutinefunction(fn)
|
|
43
46
|
|
|
44
|
-
# Get schema from TypeAdapter - will fail if function isn't properly typed
|
|
45
|
-
parameters = TypeAdapter(fn).json_schema()
|
|
46
|
-
|
|
47
47
|
# Find context parameter if it exists
|
|
48
48
|
if context_kwarg is None:
|
|
49
49
|
sig = inspect.signature(fn)
|
|
@@ -52,14 +52,18 @@ class Tool(BaseModel):
|
|
|
52
52
|
context_kwarg = param_name
|
|
53
53
|
break
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
func_arg_metadata = func_metadata(
|
|
56
|
+
fn,
|
|
57
|
+
skip_names=[context_kwarg] if context_kwarg is not None else [],
|
|
58
|
+
)
|
|
59
|
+
parameters = func_arg_metadata.arg_model.model_json_schema()
|
|
57
60
|
|
|
58
61
|
return cls(
|
|
59
62
|
fn=fn,
|
|
60
63
|
name=func_name,
|
|
61
64
|
description=func_doc,
|
|
62
65
|
parameters=parameters,
|
|
66
|
+
fn_metadata=func_arg_metadata,
|
|
63
67
|
is_async=is_async,
|
|
64
68
|
context_kwarg=context_kwarg,
|
|
65
69
|
)
|
|
@@ -67,13 +71,13 @@ class Tool(BaseModel):
|
|
|
67
71
|
async def run(self, arguments: dict, context: Optional["Context"] = None) -> Any:
|
|
68
72
|
"""Run the tool with arguments."""
|
|
69
73
|
try:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
return await self.fn_metadata.call_fn_with_arg_validation(
|
|
75
|
+
self.fn,
|
|
76
|
+
self.is_async,
|
|
77
|
+
arguments,
|
|
78
|
+
{self.context_kwarg: context}
|
|
79
|
+
if self.context_kwarg is not None
|
|
80
|
+
else None,
|
|
81
|
+
)
|
|
78
82
|
except Exception as e:
|
|
79
83
|
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable, Sequence, Awaitable
|
|
3
|
+
from typing import (
|
|
4
|
+
Annotated,
|
|
5
|
+
Any,
|
|
6
|
+
Dict,
|
|
7
|
+
ForwardRef,
|
|
8
|
+
)
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from fastmcp.exceptions import InvalidSignature
|
|
11
|
+
from pydantic._internal._typing_extra import try_eval_type
|
|
12
|
+
import json
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from pydantic.fields import FieldInfo
|
|
15
|
+
from pydantic import ConfigDict, create_model
|
|
16
|
+
from pydantic import WithJsonSchema
|
|
17
|
+
from pydantic_core import PydanticUndefined
|
|
18
|
+
from fastmcp.utilities.logging import get_logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArgModelBase(BaseModel):
|
|
25
|
+
"""A model representing the arguments to a function."""
|
|
26
|
+
|
|
27
|
+
def model_dump_one_level(self) -> dict[str, Any]:
|
|
28
|
+
"""Return a dict of the model's fields, one level deep.
|
|
29
|
+
|
|
30
|
+
That is, sub-models etc are not dumped - they are kept as pydantic models.
|
|
31
|
+
"""
|
|
32
|
+
kwargs: dict[str, Any] = {}
|
|
33
|
+
for field_name in self.model_fields.keys():
|
|
34
|
+
kwargs[field_name] = getattr(self, field_name)
|
|
35
|
+
return kwargs
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(
|
|
38
|
+
arbitrary_types_allowed=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FuncMetadata(BaseModel):
|
|
43
|
+
arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)]
|
|
44
|
+
# We can add things in the future like
|
|
45
|
+
# - Maybe some args are excluded from attempting to parse from JSON
|
|
46
|
+
# - Maybe some args are special (like context) for dependency injection
|
|
47
|
+
|
|
48
|
+
async def call_fn_with_arg_validation(
|
|
49
|
+
self,
|
|
50
|
+
fn: Callable | Awaitable,
|
|
51
|
+
fn_is_async: bool,
|
|
52
|
+
arguments_to_validate: dict[str, Any],
|
|
53
|
+
arguments_to_pass_directly: dict[str, Any] | None,
|
|
54
|
+
) -> Any:
|
|
55
|
+
"""Call the given function with arguments validated and injected.
|
|
56
|
+
|
|
57
|
+
Arguments are first attempted to be parsed from JSON, then validated against
|
|
58
|
+
the argument model, before being passed to the function.
|
|
59
|
+
"""
|
|
60
|
+
arguments_pre_parsed = self.pre_parse_json(arguments_to_validate)
|
|
61
|
+
arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed)
|
|
62
|
+
arguments_parsed_dict = arguments_parsed_model.model_dump_one_level()
|
|
63
|
+
|
|
64
|
+
arguments_parsed_dict |= arguments_to_pass_directly or {}
|
|
65
|
+
|
|
66
|
+
if fn_is_async:
|
|
67
|
+
return await fn(**arguments_parsed_dict)
|
|
68
|
+
return fn(**arguments_parsed_dict)
|
|
69
|
+
|
|
70
|
+
def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
71
|
+
"""Pre-parse data from JSON.
|
|
72
|
+
|
|
73
|
+
Return a dict with same keys as input but with values parsed from JSON
|
|
74
|
+
if appropriate.
|
|
75
|
+
|
|
76
|
+
This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside
|
|
77
|
+
a string rather than an actual list. Claude desktop is prone to this - in fact
|
|
78
|
+
it seems incapable of NOT doing this. For sub-models, it tends to pass
|
|
79
|
+
dicts (JSON objects) as JSON strings, which can be pre-parsed here.
|
|
80
|
+
"""
|
|
81
|
+
new_data = data.copy() # Shallow copy
|
|
82
|
+
for field_name, field_info in self.arg_model.model_fields.items():
|
|
83
|
+
if field_name not in data.keys():
|
|
84
|
+
continue
|
|
85
|
+
if isinstance(data[field_name], str):
|
|
86
|
+
try:
|
|
87
|
+
pre_parsed = json.loads(data[field_name])
|
|
88
|
+
except json.JSONDecodeError:
|
|
89
|
+
continue # Not JSON - skip
|
|
90
|
+
if isinstance(pre_parsed, str):
|
|
91
|
+
# This is likely that the raw value is e.g. `"hello"` which we
|
|
92
|
+
# Should really be parsed as '"hello"' in Python - but if we parse
|
|
93
|
+
# it as JSON it'll turn into just 'hello'. So we skip it.
|
|
94
|
+
continue
|
|
95
|
+
new_data[field_name] = pre_parsed
|
|
96
|
+
assert new_data.keys() == data.keys()
|
|
97
|
+
return new_data
|
|
98
|
+
|
|
99
|
+
model_config = ConfigDict(
|
|
100
|
+
arbitrary_types_allowed=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata:
|
|
105
|
+
"""Given a function, return metadata including a pydantic model representing its signature.
|
|
106
|
+
|
|
107
|
+
The use case for this is
|
|
108
|
+
```
|
|
109
|
+
meta = func_to_pyd(func)
|
|
110
|
+
validated_args = meta.arg_model.model_validate(some_raw_data_dict)
|
|
111
|
+
return func(**validated_args.model_dump_one_level())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**critically** it also provides pre-parse helper to attempt to parse things from JSON.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
func: The function to convert to a pydantic model
|
|
118
|
+
skip_names: A list of parameter names to skip. These will not be included in
|
|
119
|
+
the model.
|
|
120
|
+
Returns:
|
|
121
|
+
A pydantic model representing the function's signature.
|
|
122
|
+
"""
|
|
123
|
+
sig = _get_typed_signature(func)
|
|
124
|
+
params = sig.parameters
|
|
125
|
+
dynamic_pydantic_model_params: dict[str, Any] = {}
|
|
126
|
+
for param in params.values():
|
|
127
|
+
if param.name.startswith("_"):
|
|
128
|
+
raise InvalidSignature(
|
|
129
|
+
f"Parameter {param.name} of {func.__name__} may not start with an underscore"
|
|
130
|
+
)
|
|
131
|
+
if param.name in skip_names:
|
|
132
|
+
continue
|
|
133
|
+
annotation = param.annotation
|
|
134
|
+
|
|
135
|
+
# `x: None` / `x: None = None`
|
|
136
|
+
if annotation is None:
|
|
137
|
+
annotation = Annotated[
|
|
138
|
+
None,
|
|
139
|
+
Field(
|
|
140
|
+
default=param.default
|
|
141
|
+
if param.default is not inspect.Parameter.empty
|
|
142
|
+
else PydanticUndefined
|
|
143
|
+
),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
# Untyped field
|
|
147
|
+
if annotation is inspect.Parameter.empty:
|
|
148
|
+
annotation = Annotated[
|
|
149
|
+
Any,
|
|
150
|
+
Field(),
|
|
151
|
+
# 🤷
|
|
152
|
+
WithJsonSchema({"title": param.name, "type": "string"}),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
field_info = FieldInfo.from_annotated_attribute(
|
|
156
|
+
annotation,
|
|
157
|
+
param.default
|
|
158
|
+
if param.default is not inspect.Parameter.empty
|
|
159
|
+
else PydanticUndefined,
|
|
160
|
+
)
|
|
161
|
+
dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
arguments_model = create_model(
|
|
165
|
+
f"{func.__name__}Arguments",
|
|
166
|
+
**dynamic_pydantic_model_params,
|
|
167
|
+
__base__=ArgModelBase,
|
|
168
|
+
)
|
|
169
|
+
resp = FuncMetadata(arg_model=arguments_model)
|
|
170
|
+
return resp
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
|
|
174
|
+
if isinstance(annotation, str):
|
|
175
|
+
annotation = ForwardRef(annotation)
|
|
176
|
+
annotation, status = try_eval_type(annotation, globalns, globalns)
|
|
177
|
+
|
|
178
|
+
# This check and raise could perhaps be skipped, and we (FastMCP) just call
|
|
179
|
+
# model_rebuild right before using it 🤷
|
|
180
|
+
if status is False:
|
|
181
|
+
raise InvalidSignature(f"Unable to evaluate type annotation {annotation}")
|
|
182
|
+
|
|
183
|
+
return annotation
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
|
187
|
+
"""Get function signature while evaluating forward references"""
|
|
188
|
+
signature = inspect.signature(call)
|
|
189
|
+
globalns = getattr(call, "__globals__", {})
|
|
190
|
+
typed_params = [
|
|
191
|
+
inspect.Parameter(
|
|
192
|
+
name=param.name,
|
|
193
|
+
kind=param.kind,
|
|
194
|
+
default=param.default,
|
|
195
|
+
annotation=_get_typed_annotation(param.annotation, globalns),
|
|
196
|
+
)
|
|
197
|
+
for param in signature.parameters.values()
|
|
198
|
+
]
|
|
199
|
+
typed_signature = inspect.Signature(typed_params)
|
|
200
|
+
return typed_signature
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: A more ergonomic interface for MCP servers
|
|
5
5
|
Author: Jeremiah Lowin
|
|
6
6
|
License: Apache-2.0
|
|
@@ -20,6 +20,12 @@ Requires-Dist: pytest-asyncio>=0.23.5; extra == 'dev'
|
|
|
20
20
|
Requires-Dist: pytest-xdist>=3.6.1; extra == 'dev'
|
|
21
21
|
Requires-Dist: pytest>=8.3.3; extra == 'dev'
|
|
22
22
|
Requires-Dist: ruff; extra == 'dev'
|
|
23
|
+
Provides-Extra: tests
|
|
24
|
+
Requires-Dist: pre-commit; extra == 'tests'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23.5; extra == 'tests'
|
|
26
|
+
Requires-Dist: pytest-xdist>=3.6.1; extra == 'tests'
|
|
27
|
+
Requires-Dist: pytest>=8.3.3; extra == 'tests'
|
|
28
|
+
Requires-Dist: ruff; extra == 'tests'
|
|
23
29
|
Description-Content-Type: text/markdown
|
|
24
30
|
|
|
25
31
|
<!-- omit in toc -->
|
|
@@ -122,6 +128,8 @@ pip install fastmcp
|
|
|
122
128
|
Let's create a simple MCP server that exposes a calculator tool and some data:
|
|
123
129
|
|
|
124
130
|
```python
|
|
131
|
+
# server.py
|
|
132
|
+
|
|
125
133
|
from fastmcp import FastMCP
|
|
126
134
|
|
|
127
135
|
|
|
@@ -143,14 +151,12 @@ def get_greeting(name: str) -> str:
|
|
|
143
151
|
return f"Hello, {name}!"
|
|
144
152
|
```
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
1. Install it in [Claude Desktop](https://claude.ai/download):
|
|
154
|
+
You can install this server in [Claude Desktop](https://claude.ai/download) and interact with it right away by running:
|
|
149
155
|
```bash
|
|
150
156
|
fastmcp install server.py
|
|
151
157
|
```
|
|
152
158
|
|
|
153
|
-
|
|
159
|
+
Alternatively, you can test it with the MCP Inspector:
|
|
154
160
|
```bash
|
|
155
161
|
fastmcp dev server.py
|
|
156
162
|
```
|
|
@@ -181,9 +187,6 @@ from fastmcp import FastMCP
|
|
|
181
187
|
# Create a named server
|
|
182
188
|
mcp = FastMCP("My App")
|
|
183
189
|
|
|
184
|
-
# Configure host/port for HTTP transport (optional)
|
|
185
|
-
mcp = FastMCP("My App", host="localhost", port=8000)
|
|
186
|
-
|
|
187
190
|
# Specify dependencies for deployment and development
|
|
188
191
|
mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
|
|
189
192
|
```
|
|
@@ -239,6 +242,27 @@ async def fetch_weather(city: str) -> str:
|
|
|
239
242
|
return response.text
|
|
240
243
|
```
|
|
241
244
|
|
|
245
|
+
Complex input handling example:
|
|
246
|
+
```python
|
|
247
|
+
from pydantic import BaseModel, Field
|
|
248
|
+
from typing import Annotated
|
|
249
|
+
|
|
250
|
+
class ShrimpTank(BaseModel):
|
|
251
|
+
class Shrimp(BaseModel):
|
|
252
|
+
name: Annotated[str, Field(max_length=10)]
|
|
253
|
+
|
|
254
|
+
shrimp: list[Shrimp]
|
|
255
|
+
|
|
256
|
+
@mcp.tool()
|
|
257
|
+
def name_shrimp(
|
|
258
|
+
tank: ShrimpTank,
|
|
259
|
+
# You can use pydantic Field in function signatures for validation.
|
|
260
|
+
extra_names: Annotated[list[str], Field(max_length=10)],
|
|
261
|
+
) -> list[str]:
|
|
262
|
+
"""List all shrimp names in the tank"""
|
|
263
|
+
return [shrimp.name for shrimp in tank.shrimp] + extra_names
|
|
264
|
+
```
|
|
265
|
+
|
|
242
266
|
### Prompts
|
|
243
267
|
|
|
244
268
|
Prompts are reusable templates that help LLMs interact with your server effectively. They're like "best practices" encoded into your server. A prompt can be as simple as a string:
|
|
@@ -491,23 +515,20 @@ FastMCP requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
|
|
|
491
515
|
|
|
492
516
|
### Installation
|
|
493
517
|
|
|
494
|
-
|
|
518
|
+
For development, we recommend installing FastMCP with development dependencies, which includes various utilities the maintainers find useful.
|
|
495
519
|
|
|
496
520
|
```bash
|
|
497
|
-
git clone https://github.com/
|
|
521
|
+
git clone https://github.com/jlowin/fastmcp.git
|
|
498
522
|
cd fastmcp
|
|
523
|
+
uv sync --frozen --extra dev
|
|
499
524
|
```
|
|
500
525
|
|
|
501
|
-
|
|
526
|
+
For running tests only (e.g., in CI), you only need the testing dependencies:
|
|
502
527
|
|
|
503
528
|
```bash
|
|
504
|
-
uv
|
|
505
|
-
source .venv/bin/activate
|
|
506
|
-
uv sync --frozen --all-extras --dev
|
|
529
|
+
uv sync --frozen --extra tests
|
|
507
530
|
```
|
|
508
531
|
|
|
509
|
-
|
|
510
|
-
|
|
511
532
|
### Testing
|
|
512
533
|
|
|
513
534
|
Please make sure to test any new functionality. Your tests should be simple and atomic and anticipate change rather than cement complex patterns.
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
fastmcp/__init__.py,sha256=Y5dHGBwyQPgNP5gzOyNIItefvMZ3vJLdom1oV8A1u_k,248
|
|
2
|
-
fastmcp/exceptions.py,sha256=
|
|
2
|
+
fastmcp/exceptions.py,sha256=q9djUDmpwmGEWcHI8q4UzJBtf7s7UtgL--OB7OaGzyQ,435
|
|
3
3
|
fastmcp/server.py,sha256=V6BaciC5u9g_XzbvPkafjGO8OtEBPGy_KlV535Au0do,22234
|
|
4
4
|
fastmcp/cli/__init__.py,sha256=7hrwtCHX9nMd9qcz7R_JFSoqbL71fC35cBLXBS430mg,88
|
|
5
|
-
fastmcp/cli/claude.py,sha256=
|
|
6
|
-
fastmcp/cli/cli.py,sha256=
|
|
5
|
+
fastmcp/cli/claude.py,sha256=5SoVEsA_PnOyOe2bcItvfcCwuhfX6W99TP1nXahLIJE,4442
|
|
6
|
+
fastmcp/cli/cli.py,sha256=sb8RC8YnKg9dlH9NHzX3owLYKVC1D8Mt6OhQj_G8sGU,13297
|
|
7
7
|
fastmcp/prompts/__init__.py,sha256=4BsMxoYolpoxg74xkkkzCFL8vvdkLVJ5cIPNs1ND1Jo,99
|
|
8
8
|
fastmcp/prompts/base.py,sha256=WaSsfyFSsUPUbcApkGy3Pm-Ne-Gk-5ZwU3efqRYn1mQ,4996
|
|
9
9
|
fastmcp/prompts/manager.py,sha256=EkexOB_N4QNtC-UlZmIcWcau91ceO2O1K4_kD75pA_A,1485
|
|
10
10
|
fastmcp/prompts/prompt_manager.py,sha256=5uR14gsi7l0YHwbxFH7N5b_ACHHRWyTtBAH3R0-G5rk,1129
|
|
11
11
|
fastmcp/resources/__init__.py,sha256=9QShop6ckX3Khh3BQLZNkB6R2ZhwskAcnh7-sIuX-W8,464
|
|
12
|
-
fastmcp/resources/base.py,sha256=
|
|
12
|
+
fastmcp/resources/base.py,sha256=inm2uhaE6KPQEsMd4SyDLbRIOjQrXahSn84RNT3Y6MY,1723
|
|
13
13
|
fastmcp/resources/resource_manager.py,sha256=b0PKpG-pKi7x2Yx-qaeknjv0mqL17zixSIYOz2V5G6o,3271
|
|
14
14
|
fastmcp/resources/templates.py,sha256=EmLlI-ddBBzSTAUiA6-knFnHCE3MPMW2ZoH9WswPKvI,2868
|
|
15
15
|
fastmcp/resources/types.py,sha256=ofE6bfeQQfPSmaWrLGDf3qjCP0kGjKmvupsHDYkSrj0,5658
|
|
16
16
|
fastmcp/tools/__init__.py,sha256=ZboxhyMJDl87Svjov8YwNYwNZi55P9VhmpTjBZLesnk,96
|
|
17
|
-
fastmcp/tools/base.py,sha256=
|
|
17
|
+
fastmcp/tools/base.py,sha256=aJEOnNUs5KiTC16Szr8AEZyozTtpp0vZXtF5wMIA2mM,2797
|
|
18
18
|
fastmcp/tools/tool_manager.py,sha256=PT6XHcQWzhdC6kfdsJaddRn7VLps4nAs5FMG8l1j8Zc,1617
|
|
19
19
|
fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
|
|
20
|
+
fastmcp/utilities/func_metadata.py,sha256=TWxO9dVqdcJp4STGlm-WMekwpENqaGUIETstw_GTJ7U,7225
|
|
20
21
|
fastmcp/utilities/logging.py,sha256=VLJdNc0tIYoQZmpobehLUnWrQz7NXnuwSqrDlFt2RF0,738
|
|
21
22
|
fastmcp/utilities/types.py,sha256=jFlZMZsKrJg4NWc1vTBIILLoHpTVwSd-vxO7ycoRuig,1718
|
|
22
|
-
fastmcp-0.3.
|
|
23
|
-
fastmcp-0.3.
|
|
24
|
-
fastmcp-0.3.
|
|
25
|
-
fastmcp-0.3.
|
|
26
|
-
fastmcp-0.3.
|
|
23
|
+
fastmcp-0.3.5.dist-info/METADATA,sha256=bOVsMw4T9aVzb0AgpyCNIQibIYQPkJIM8Hb_ETfwue8,16174
|
|
24
|
+
fastmcp-0.3.5.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
25
|
+
fastmcp-0.3.5.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
26
|
+
fastmcp-0.3.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
27
|
+
fastmcp-0.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|