fastmcp 2.2.2__py3-none-any.whl → 2.2.4__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 +36 -3
- fastmcp/resources/template.py +6 -3
- fastmcp/server/openapi.py +13 -59
- fastmcp/tools/tool.py +6 -1
- fastmcp/utilities/func_metadata.py +16 -4
- {fastmcp-2.2.2.dist-info → fastmcp-2.2.4.dist-info}/METADATA +20 -20
- {fastmcp-2.2.2.dist-info → fastmcp-2.2.4.dist-info}/RECORD +10 -10
- {fastmcp-2.2.2.dist-info → fastmcp-2.2.4.dist-info}/WHEEL +0 -0
- {fastmcp-2.2.2.dist-info → fastmcp-2.2.4.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.2.2.dist-info → fastmcp-2.2.4.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
|
@@ -223,6 +223,27 @@ def dev(
|
|
|
223
223
|
help="Additional packages to install",
|
|
224
224
|
),
|
|
225
225
|
] = [],
|
|
226
|
+
inspector_version: Annotated[
|
|
227
|
+
str | None,
|
|
228
|
+
typer.Option(
|
|
229
|
+
"--inspector-version",
|
|
230
|
+
help="Version of the MCP Inspector to use",
|
|
231
|
+
),
|
|
232
|
+
] = None,
|
|
233
|
+
ui_port: Annotated[
|
|
234
|
+
int | None,
|
|
235
|
+
typer.Option(
|
|
236
|
+
"--ui-port",
|
|
237
|
+
help="Port for the MCP Inspector UI",
|
|
238
|
+
),
|
|
239
|
+
] = None,
|
|
240
|
+
server_port: Annotated[
|
|
241
|
+
int | None,
|
|
242
|
+
typer.Option(
|
|
243
|
+
"--server-port",
|
|
244
|
+
help="Port for the MCP Inspector Proxy server",
|
|
245
|
+
),
|
|
246
|
+
] = None,
|
|
226
247
|
) -> None:
|
|
227
248
|
"""Run a MCP server with the MCP Inspector."""
|
|
228
249
|
file, server_object = _parse_file_path(file_spec)
|
|
@@ -234,6 +255,8 @@ def dev(
|
|
|
234
255
|
"server_object": server_object,
|
|
235
256
|
"with_editable": str(with_editable) if with_editable else None,
|
|
236
257
|
"with_packages": with_packages,
|
|
258
|
+
"ui_port": ui_port,
|
|
259
|
+
"server_port": server_port,
|
|
237
260
|
},
|
|
238
261
|
)
|
|
239
262
|
|
|
@@ -243,7 +266,11 @@ def dev(
|
|
|
243
266
|
if hasattr(server, "dependencies"):
|
|
244
267
|
with_packages = list(set(with_packages + server.dependencies))
|
|
245
268
|
|
|
246
|
-
|
|
269
|
+
env_vars = {}
|
|
270
|
+
if ui_port:
|
|
271
|
+
env_vars["CLIENT_PORT"] = str(ui_port)
|
|
272
|
+
if server_port:
|
|
273
|
+
env_vars["SERVER_PORT"] = str(server_port)
|
|
247
274
|
|
|
248
275
|
# Get the correct npx command
|
|
249
276
|
npx_cmd = _get_npx_command()
|
|
@@ -254,13 +281,19 @@ def dev(
|
|
|
254
281
|
)
|
|
255
282
|
sys.exit(1)
|
|
256
283
|
|
|
284
|
+
inspector_cmd = "@modelcontextprotocol/inspector"
|
|
285
|
+
if inspector_version:
|
|
286
|
+
inspector_cmd += f"@{inspector_version}"
|
|
287
|
+
|
|
288
|
+
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
|
289
|
+
|
|
257
290
|
# Run the MCP Inspector command with shell=True on Windows
|
|
258
291
|
shell = sys.platform == "win32"
|
|
259
292
|
process = subprocess.run(
|
|
260
|
-
[npx_cmd,
|
|
293
|
+
[npx_cmd, inspector_cmd] + uv_cmd,
|
|
261
294
|
check=True,
|
|
262
295
|
shell=shell,
|
|
263
|
-
env=dict(os.environ.items())
|
|
296
|
+
env=dict(os.environ.items()) | env_vars,
|
|
264
297
|
)
|
|
265
298
|
sys.exit(process.returncode)
|
|
266
299
|
except subprocess.CalledProcessError as e:
|
fastmcp/resources/template.py
CHANGED
|
@@ -24,13 +24,16 @@ from fastmcp.utilities.types import _convert_set_defaults
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def build_regex(template: str) -> re.Pattern:
|
|
27
|
-
# Escape all non-brace characters, then restore {var} placeholders
|
|
28
27
|
parts = re.split(r"(\{[^}]+\})", template)
|
|
29
28
|
pattern = ""
|
|
30
29
|
for part in parts:
|
|
31
30
|
if part.startswith("{") and part.endswith("}"):
|
|
32
31
|
name = part[1:-1]
|
|
33
|
-
|
|
32
|
+
if name.endswith("*"):
|
|
33
|
+
name = name[:-1]
|
|
34
|
+
pattern += f"(?P<{name}>.+)"
|
|
35
|
+
else:
|
|
36
|
+
pattern += f"(?P<{name}>[^/]+)"
|
|
34
37
|
else:
|
|
35
38
|
pattern += re.escape(part)
|
|
36
39
|
return re.compile(f"^{pattern}$")
|
|
@@ -92,7 +95,7 @@ class ResourceTemplate(BaseModel):
|
|
|
92
95
|
raise ValueError("You must provide a name for lambda functions")
|
|
93
96
|
|
|
94
97
|
# Validate that URI params match function params
|
|
95
|
-
uri_params = set(re.findall(r"{(\w+)}", uri_template))
|
|
98
|
+
uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
|
96
99
|
if not uri_params:
|
|
97
100
|
raise ValueError("URI template must contain at least one parameter")
|
|
98
101
|
|
fastmcp/server/openapi.py
CHANGED
|
@@ -257,7 +257,7 @@ class OpenAPIResource(Resource):
|
|
|
257
257
|
self._client = client
|
|
258
258
|
self._route = route
|
|
259
259
|
|
|
260
|
-
async def read(self) -> str:
|
|
260
|
+
async def read(self) -> str | bytes:
|
|
261
261
|
"""Fetch the resource data by making an HTTP request."""
|
|
262
262
|
try:
|
|
263
263
|
# Extract path parameters from the URI if present
|
|
@@ -297,15 +297,16 @@ class OpenAPIResource(Resource):
|
|
|
297
297
|
# Raise for 4xx/5xx responses
|
|
298
298
|
response.raise_for_status()
|
|
299
299
|
|
|
300
|
-
#
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
else:
|
|
300
|
+
# Determine content type and return appropriate format
|
|
301
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
302
|
+
|
|
303
|
+
if "application/json" in content_type:
|
|
304
|
+
result = response.json()
|
|
305
|
+
return json.dumps(result)
|
|
306
|
+
elif any(ct in content_type for ct in ["text/", "application/xml"]):
|
|
308
307
|
return response.text
|
|
308
|
+
else:
|
|
309
|
+
return response.content
|
|
309
310
|
|
|
310
311
|
except httpx.HTTPStatusError as e:
|
|
311
312
|
# Handle HTTP errors (4xx, 5xx)
|
|
@@ -343,59 +344,13 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
343
344
|
uri_template=uri_template,
|
|
344
345
|
name=name,
|
|
345
346
|
description=description,
|
|
346
|
-
fn=
|
|
347
|
+
fn=lambda **kwargs: None,
|
|
347
348
|
parameters=parameters,
|
|
348
349
|
tags=tags,
|
|
349
350
|
)
|
|
350
351
|
self._client = client
|
|
351
352
|
self._route = route
|
|
352
353
|
|
|
353
|
-
async def _create_resource_fn(self, **kwargs):
|
|
354
|
-
"""Create a resource with parameters."""
|
|
355
|
-
# Prepare the path with parameters
|
|
356
|
-
path = self._route.path
|
|
357
|
-
for param_name, param_value in kwargs.items():
|
|
358
|
-
path = path.replace(f"{{{param_name}}}", str(param_value))
|
|
359
|
-
|
|
360
|
-
try:
|
|
361
|
-
response = await self._client.request(
|
|
362
|
-
method=self._route.method,
|
|
363
|
-
url=path,
|
|
364
|
-
timeout=30.0, # Default timeout
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
# Raise for 4xx/5xx responses
|
|
368
|
-
response.raise_for_status()
|
|
369
|
-
|
|
370
|
-
# Determine the mime type from the response
|
|
371
|
-
content_type = response.headers.get("content-type", "application/json")
|
|
372
|
-
mime_type = content_type.split(";")[0].strip()
|
|
373
|
-
|
|
374
|
-
# Return the appropriate data
|
|
375
|
-
if mime_type == "application/json":
|
|
376
|
-
try:
|
|
377
|
-
return response.json()
|
|
378
|
-
except (json.JSONDecodeError, ValueError):
|
|
379
|
-
return response.text
|
|
380
|
-
else:
|
|
381
|
-
return response.text
|
|
382
|
-
|
|
383
|
-
except httpx.HTTPStatusError as e:
|
|
384
|
-
error_message = (
|
|
385
|
-
f"HTTP error {e.response.status_code}: {e.response.reason_phrase}"
|
|
386
|
-
)
|
|
387
|
-
try:
|
|
388
|
-
error_data = e.response.json()
|
|
389
|
-
error_message += f" - {error_data}"
|
|
390
|
-
except (json.JSONDecodeError, ValueError):
|
|
391
|
-
if e.response.text:
|
|
392
|
-
error_message += f" - {e.response.text}"
|
|
393
|
-
|
|
394
|
-
raise ValueError(error_message)
|
|
395
|
-
|
|
396
|
-
except httpx.RequestError as e:
|
|
397
|
-
raise ValueError(f"Request error: {str(e)}")
|
|
398
|
-
|
|
399
354
|
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
400
355
|
"""Create a resource with the given parameters."""
|
|
401
356
|
# Generate a URI for this resource instance
|
|
@@ -409,9 +364,8 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
409
364
|
route=self._route,
|
|
410
365
|
uri=uri,
|
|
411
366
|
name=f"{self.name}-{'-'.join(uri_parts)}",
|
|
412
|
-
description=self.description
|
|
413
|
-
|
|
414
|
-
mime_type="application/json", # Default, will be updated when read
|
|
367
|
+
description=self.description or f"Resource for {self._route.path}",
|
|
368
|
+
mime_type="application/json",
|
|
415
369
|
tags=set(self._route.tags or []),
|
|
416
370
|
)
|
|
417
371
|
|
fastmcp/tools/tool.py
CHANGED
|
@@ -76,7 +76,12 @@ class Tool(BaseModel):
|
|
|
76
76
|
fn_callable,
|
|
77
77
|
skip_names=[context_kwarg] if context_kwarg is not None else [],
|
|
78
78
|
)
|
|
79
|
-
|
|
79
|
+
try:
|
|
80
|
+
parameters = func_arg_metadata.arg_model.model_json_schema()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise TypeError(
|
|
83
|
+
f'Unable to parse parameters for function "{fn.__name__}": {e}'
|
|
84
|
+
) from e
|
|
80
85
|
|
|
81
86
|
return cls(
|
|
82
87
|
fn=fn_callable,
|
|
@@ -7,7 +7,15 @@ from typing import (
|
|
|
7
7
|
ForwardRef,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
from pydantic import
|
|
10
|
+
from pydantic import (
|
|
11
|
+
BaseModel,
|
|
12
|
+
ConfigDict,
|
|
13
|
+
Field,
|
|
14
|
+
TypeAdapter,
|
|
15
|
+
ValidationError,
|
|
16
|
+
WithJsonSchema,
|
|
17
|
+
create_model,
|
|
18
|
+
)
|
|
11
19
|
from pydantic._internal._typing_extra import eval_type_backport
|
|
12
20
|
from pydantic.fields import FieldInfo
|
|
13
21
|
from pydantic_core import PydanticUndefined
|
|
@@ -80,14 +88,18 @@ class FuncMetadata(BaseModel):
|
|
|
80
88
|
dicts (JSON objects) as JSON strings, which can be pre-parsed here.
|
|
81
89
|
"""
|
|
82
90
|
new_data = data.copy() # Shallow copy
|
|
83
|
-
for field_name,
|
|
91
|
+
for field_name, field_info in self.arg_model.model_fields.items():
|
|
84
92
|
if field_name not in data.keys():
|
|
85
93
|
continue
|
|
86
94
|
if isinstance(data[field_name], str):
|
|
87
95
|
try:
|
|
88
96
|
pre_parsed = json.loads(data[field_name])
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
|
|
98
|
+
# Check if the pre_parsed value is valid for the field
|
|
99
|
+
validator = TypeAdapter(field_info.annotation)
|
|
100
|
+
validator.validate_python(pre_parsed)
|
|
101
|
+
except (json.JSONDecodeError, ValidationError):
|
|
102
|
+
continue # Not JSON or invalid for the field
|
|
91
103
|
if isinstance(pre_parsed, str | int | float):
|
|
92
104
|
# This is likely that the raw value is e.g. `"hello"` which we
|
|
93
105
|
# Should really be parsed as '"hello"' in Python - but if we parse
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: The fast, Pythonic way to build MCP servers.
|
|
5
5
|
Project-URL: Homepage, https://gofastmcp.com
|
|
6
6
|
Project-URL: Repository, https://github.com/jlowin/fastmcp
|
|
@@ -141,7 +141,7 @@ FastMCP aims to be:
|
|
|
141
141
|
### Servers
|
|
142
142
|
- **Create** servers with minimal boilerplate using intuitive decorators
|
|
143
143
|
- **Proxy** existing servers to modify configuration or transport
|
|
144
|
-
- **Compose**
|
|
144
|
+
- **Compose** servers into complex applications
|
|
145
145
|
- **Generate** servers from OpenAPI specs or FastAPI objects
|
|
146
146
|
|
|
147
147
|
### Clients
|
|
@@ -361,32 +361,32 @@ The `Context` object provides:
|
|
|
361
361
|
|
|
362
362
|
### Images
|
|
363
363
|
|
|
364
|
-
Easily handle image
|
|
364
|
+
Easily handle image outputs using the `fastmcp.Image` helper class.
|
|
365
|
+
|
|
366
|
+
<Tip>
|
|
367
|
+
The below code requires the `pillow` library to be installed.
|
|
368
|
+
</Tip>
|
|
365
369
|
|
|
366
370
|
```python
|
|
367
|
-
from fastmcp import FastMCP, Image
|
|
368
|
-
from
|
|
369
|
-
|
|
371
|
+
from mcp.server.fastmcp import FastMCP, Image
|
|
372
|
+
from io import BytesIO
|
|
373
|
+
try:
|
|
374
|
+
from PIL import Image as PILImage
|
|
375
|
+
except ImportError:
|
|
376
|
+
raise ImportError("Please install the `pillow` library to run this example.")
|
|
370
377
|
|
|
371
|
-
mcp = FastMCP("
|
|
378
|
+
mcp = FastMCP("My App")
|
|
372
379
|
|
|
373
380
|
@mcp.tool()
|
|
374
|
-
def create_thumbnail(
|
|
375
|
-
"""
|
|
376
|
-
img = PILImage.open(
|
|
377
|
-
img.thumbnail((100, 100))
|
|
378
|
-
buffer =
|
|
381
|
+
def create_thumbnail(image_path: str) -> Image:
|
|
382
|
+
"""Create a thumbnail from an image"""
|
|
383
|
+
img = PILImage.open(image_path)
|
|
384
|
+
img.thumbnail((100, 100))
|
|
385
|
+
buffer = BytesIO()
|
|
379
386
|
img.save(buffer, format="PNG")
|
|
380
|
-
# Return a new Image object with the thumbnail data
|
|
381
387
|
return Image(data=buffer.getvalue(), format="png")
|
|
382
|
-
|
|
383
|
-
@mcp.tool()
|
|
384
|
-
def load_image_from_disk(path: str) -> Image:
|
|
385
|
-
"""Loads an image from the specified path."""
|
|
386
|
-
# Handles reading file and detecting format based on extension
|
|
387
|
-
return Image(path=path)
|
|
388
388
|
```
|
|
389
|
-
|
|
389
|
+
Return the `Image` helper class from your tool to send an image to the client. The `Image` helper class handles the conversion to/from the base64-encoded format required by the MCP protocol. It works with either a path to an image file, or a bytes object.
|
|
390
390
|
|
|
391
391
|
|
|
392
392
|
### MCP Clients
|
|
@@ -4,7 +4,7 @@ fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
4
4
|
fastmcp/settings.py,sha256=VCjc-3011pKRYjt2h9rZ68XhVEekbpyLyVUREVBTSrg,1955
|
|
5
5
|
fastmcp/cli/__init__.py,sha256=Ii284TNoG5lxTP40ETMGhHEq3lQZWxu9m9JuU57kUpQ,87
|
|
6
6
|
fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
|
|
7
|
-
fastmcp/cli/cli.py,sha256=
|
|
7
|
+
fastmcp/cli/cli.py,sha256=wsFIYTv48_nr0mcW8vjI1l7_thr4cOrRxzo9g-qr2l8,15726
|
|
8
8
|
fastmcp/client/__init__.py,sha256=BXO9NUhntZ5GnUACfaRCzDJ5IzxqFJs8qKG-CRMSco4,490
|
|
9
9
|
fastmcp/client/base.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
10
10
|
fastmcp/client/client.py,sha256=XXpN28epV9N5w-keQSDPBCdLFtZ5EPK2msxuQ6PxTmo,7732
|
|
@@ -26,24 +26,24 @@ fastmcp/prompts/prompt_manager.py,sha256=tMob9a-igjuzf6oTPLPGidFpJdg5JaPJVlYgyNk
|
|
|
26
26
|
fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
|
|
27
27
|
fastmcp/resources/resource.py,sha256=5FN2a7dpNwf7FSEYTNvQvkTxtodu1OPxSlJL-U-8yrM,2413
|
|
28
28
|
fastmcp/resources/resource_manager.py,sha256=_0itubfjYvfkA_wXKa4DQN5YpE7ejXhsE1hdt7m8XwU,9072
|
|
29
|
-
fastmcp/resources/template.py,sha256=
|
|
29
|
+
fastmcp/resources/template.py,sha256=3JL3I_pommv-K8Q0NLVMfbGAXtL7iuBaV-2yyUMxgnE,5832
|
|
30
30
|
fastmcp/resources/types.py,sha256=tigil7z-SUJMakGXzDLIGSqTepPrAsRpwqwtBA4yoUY,6168
|
|
31
31
|
fastmcp/server/__init__.py,sha256=pdkghG11VLMZiluQ-4_rl2JK1LMWmV003m9dDRUN8W4,92
|
|
32
32
|
fastmcp/server/context.py,sha256=s1885AZRipKB3VltfaO3VEtMxGefKs8fdZByj-4tbNI,7120
|
|
33
|
-
fastmcp/server/openapi.py,sha256=
|
|
33
|
+
fastmcp/server/openapi.py,sha256=44jqx5Y08BiQX4Ftd2ZCncOnt8wt9aNuBzLUXBYMc3A,20750
|
|
34
34
|
fastmcp/server/proxy.py,sha256=JHbxnOKbxyD5Jg2M_zSlNGKVBSZ5NUlVhQoKf442wxo,9619
|
|
35
35
|
fastmcp/server/server.py,sha256=PFhnwa24diSKCz8KO39q43yuSHSbqYrzgnSspc-SPfg,31721
|
|
36
36
|
fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
|
|
37
|
-
fastmcp/tools/tool.py,sha256=
|
|
37
|
+
fastmcp/tools/tool.py,sha256=hAdeQaJ-1AgPyVZPvCQKYFkK0opccJWa39xWGFAWlzA,5975
|
|
38
38
|
fastmcp/tools/tool_manager.py,sha256=hClv7fwj0cQSSwW0i-Swt7xiVqR4T9LVmr1Tp704nW4,3283
|
|
39
39
|
fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
|
|
40
40
|
fastmcp/utilities/decorators.py,sha256=AjhjsetQZF4YOPV5MTZmIxO21iFp_4fDIS3O2_KNCEg,2990
|
|
41
|
-
fastmcp/utilities/func_metadata.py,sha256=
|
|
41
|
+
fastmcp/utilities/func_metadata.py,sha256=iYXnx7MILOSL8mUQ6Rtq_6U7qA08OkoEN2APY802hJg,8141
|
|
42
42
|
fastmcp/utilities/logging.py,sha256=zav8pnFxG_fvGJHUV2XpobmT9WVrmv1mlQBSCz-CPx4,1159
|
|
43
43
|
fastmcp/utilities/openapi.py,sha256=PrH3usbTblaVC6jIH1UGiPEfgB2sSCLj33zA5dH7o_s,45193
|
|
44
44
|
fastmcp/utilities/types.py,sha256=m2rPYMzO-ZFvvZ46N-1-Xqyw693K7yq9Z2xR4pVELyk,2091
|
|
45
|
-
fastmcp-2.2.
|
|
46
|
-
fastmcp-2.2.
|
|
47
|
-
fastmcp-2.2.
|
|
48
|
-
fastmcp-2.2.
|
|
49
|
-
fastmcp-2.2.
|
|
45
|
+
fastmcp-2.2.4.dist-info/METADATA,sha256=3qgwiPo7x3pPDatCPYyvKwPfRRmtd_INFQclHkfNxDY,27769
|
|
46
|
+
fastmcp-2.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
47
|
+
fastmcp-2.2.4.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
|
|
48
|
+
fastmcp-2.2.4.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
49
|
+
fastmcp-2.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|