fastmcp 2.2.5__py3-none-any.whl → 2.2.7__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 +1 -0
- fastmcp/client/base.py +0 -1
- fastmcp/client/client.py +255 -49
- fastmcp/client/logging.py +13 -0
- fastmcp/client/sampling.py +2 -0
- fastmcp/client/transports.py +37 -4
- fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +1 -3
- fastmcp/prompts/prompt.py +8 -4
- fastmcp/resources/template.py +5 -2
- fastmcp/resources/types.py +4 -7
- fastmcp/server/context.py +27 -14
- fastmcp/server/openapi.py +94 -32
- fastmcp/server/proxy.py +4 -3
- fastmcp/server/server.py +261 -30
- fastmcp/settings.py +7 -0
- fastmcp/tools/tool.py +24 -22
- fastmcp/tools/tool_manager.py +16 -3
- fastmcp/utilities/http.py +44 -0
- fastmcp/utilities/openapi.py +147 -36
- fastmcp/utilities/types.py +29 -1
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/METADATA +4 -4
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/RECORD +25 -23
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/WHEEL +0 -0
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.2.5.dist-info → fastmcp-2.2.7.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/openapi.py
CHANGED
|
@@ -5,17 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
import enum
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
|
+
from collections.abc import Callable
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from re import Pattern
|
|
10
11
|
from typing import TYPE_CHECKING, Any, Literal
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
13
|
-
from mcp.types import TextContent
|
|
14
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
|
|
14
15
|
from pydantic.networks import AnyUrl
|
|
15
16
|
|
|
16
17
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
17
18
|
from fastmcp.server.server import FastMCP
|
|
18
|
-
from fastmcp.tools.tool import Tool
|
|
19
|
+
from fastmcp.tools.tool import Tool, _convert_to_content
|
|
19
20
|
from fastmcp.utilities import openapi
|
|
20
21
|
from fastmcp.utilities.func_metadata import func_metadata
|
|
21
22
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -125,6 +126,9 @@ class OpenAPITool(Tool):
|
|
|
125
126
|
fn_metadata: Any,
|
|
126
127
|
is_async: bool = True,
|
|
127
128
|
tags: set[str] = set(),
|
|
129
|
+
timeout: float | None = None,
|
|
130
|
+
annotations: ToolAnnotations | None = None,
|
|
131
|
+
serializer: Callable[[Any], str] | None = None,
|
|
128
132
|
):
|
|
129
133
|
super().__init__(
|
|
130
134
|
name=name,
|
|
@@ -135,9 +139,12 @@ class OpenAPITool(Tool):
|
|
|
135
139
|
is_async=is_async,
|
|
136
140
|
context_kwarg="context", # Default context keyword argument
|
|
137
141
|
tags=tags,
|
|
142
|
+
annotations=annotations,
|
|
143
|
+
serializer=serializer,
|
|
138
144
|
)
|
|
139
145
|
self._client = client
|
|
140
146
|
self._route = route
|
|
147
|
+
self._timeout = timeout
|
|
141
148
|
|
|
142
149
|
async def _execute_request(self, *args, **kwargs):
|
|
143
150
|
"""Execute the HTTP request based on the route configuration."""
|
|
@@ -147,19 +154,37 @@ class OpenAPITool(Tool):
|
|
|
147
154
|
path = self._route.path
|
|
148
155
|
|
|
149
156
|
# Replace path parameters with values from kwargs
|
|
157
|
+
# Path parameters should never be None as they're typically required
|
|
158
|
+
# but we'll handle that case anyway
|
|
150
159
|
path_params = {
|
|
151
160
|
p.name: kwargs.get(p.name)
|
|
152
161
|
for p in self._route.parameters
|
|
153
162
|
if p.location == "path"
|
|
163
|
+
and p.name in kwargs
|
|
164
|
+
and kwargs.get(p.name) is not None
|
|
154
165
|
}
|
|
166
|
+
|
|
167
|
+
# Ensure all path parameters are provided
|
|
168
|
+
required_path_params = {
|
|
169
|
+
p.name
|
|
170
|
+
for p in self._route.parameters
|
|
171
|
+
if p.location == "path" and p.required
|
|
172
|
+
}
|
|
173
|
+
missing_params = required_path_params - path_params.keys()
|
|
174
|
+
if missing_params:
|
|
175
|
+
raise ValueError(f"Missing required path parameters: {missing_params}")
|
|
176
|
+
|
|
155
177
|
for param_name, param_value in path_params.items():
|
|
156
178
|
path = path.replace(f"{{{param_name}}}", str(param_value))
|
|
157
179
|
|
|
158
|
-
# Prepare query parameters
|
|
180
|
+
# Prepare query parameters - filter out None and empty strings
|
|
159
181
|
query_params = {
|
|
160
182
|
p.name: kwargs.get(p.name)
|
|
161
183
|
for p in self._route.parameters
|
|
162
|
-
if p.location == "query"
|
|
184
|
+
if p.location == "query"
|
|
185
|
+
and p.name in kwargs
|
|
186
|
+
and kwargs.get(p.name) is not None
|
|
187
|
+
and kwargs.get(p.name) != ""
|
|
163
188
|
}
|
|
164
189
|
|
|
165
190
|
# Prepare headers - fix typing by ensuring all values are strings
|
|
@@ -206,7 +231,7 @@ class OpenAPITool(Tool):
|
|
|
206
231
|
params=query_params,
|
|
207
232
|
headers=headers,
|
|
208
233
|
json=json_data,
|
|
209
|
-
timeout=
|
|
234
|
+
timeout=self._timeout,
|
|
210
235
|
)
|
|
211
236
|
|
|
212
237
|
# Raise for 4xx/5xx responses
|
|
@@ -237,9 +262,14 @@ class OpenAPITool(Tool):
|
|
|
237
262
|
# Handle request errors (connection, timeout, etc.)
|
|
238
263
|
raise ValueError(f"Request error: {str(e)}")
|
|
239
264
|
|
|
240
|
-
async def run(
|
|
265
|
+
async def run(
|
|
266
|
+
self,
|
|
267
|
+
arguments: dict[str, Any],
|
|
268
|
+
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
269
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
241
270
|
"""Run the tool with arguments and optional context."""
|
|
242
|
-
|
|
271
|
+
response = await self._execute_request(**arguments, context=context)
|
|
272
|
+
return _convert_to_content(response)
|
|
243
273
|
|
|
244
274
|
|
|
245
275
|
class OpenAPIResource(Resource):
|
|
@@ -254,6 +284,7 @@ class OpenAPIResource(Resource):
|
|
|
254
284
|
description: str,
|
|
255
285
|
mime_type: str = "application/json",
|
|
256
286
|
tags: set[str] = set(),
|
|
287
|
+
timeout: float | None = None,
|
|
257
288
|
):
|
|
258
289
|
super().__init__(
|
|
259
290
|
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
@@ -264,6 +295,7 @@ class OpenAPIResource(Resource):
|
|
|
264
295
|
)
|
|
265
296
|
self._client = client
|
|
266
297
|
self._route = route
|
|
298
|
+
self._timeout = timeout
|
|
267
299
|
|
|
268
300
|
async def read(
|
|
269
301
|
self, context: Context[ServerSessionT, LifespanContextT] | None = None
|
|
@@ -278,30 +310,44 @@ class OpenAPIResource(Resource):
|
|
|
278
310
|
if "{" in path and "}" in path:
|
|
279
311
|
# Extract the resource ID from the URI (the last part after the last slash)
|
|
280
312
|
parts = resource_uri.split("/")
|
|
313
|
+
|
|
281
314
|
if len(parts) > 1:
|
|
282
315
|
# Find all path parameters in the route path
|
|
283
316
|
path_params = {}
|
|
284
317
|
|
|
285
|
-
#
|
|
286
|
-
param_value = parts[
|
|
287
|
-
-1
|
|
288
|
-
] # The last part contains the parameter value
|
|
289
|
-
|
|
290
|
-
# Find the path parameter name from the route path
|
|
318
|
+
# Find the path parameter names from the route path
|
|
291
319
|
param_matches = re.findall(r"\{([^}]+)\}", path)
|
|
292
320
|
if param_matches:
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
|
|
321
|
+
# Reverse sorting from creation order (traversal is backwards)
|
|
322
|
+
param_matches.sort(reverse=True)
|
|
323
|
+
# Number of sent parameters is number of parts -1 (assuming first part is resource identifier)
|
|
324
|
+
expected_param_count = len(parts) - 1
|
|
325
|
+
# Map parameters from the end of the URI to the parameters in the path
|
|
326
|
+
# Last parameter in URI (parts[-1]) maps to last parameter in path, and so on
|
|
327
|
+
for i, param_name in enumerate(param_matches):
|
|
328
|
+
# Ensure we don't use resource identifier as parameter
|
|
329
|
+
if i < expected_param_count:
|
|
330
|
+
# Get values from the end of parts
|
|
331
|
+
param_value = parts[-1 - i]
|
|
332
|
+
path_params[param_name] = param_value
|
|
296
333
|
|
|
297
334
|
# Replace path parameters with their values
|
|
298
335
|
for param_name, param_value in path_params.items():
|
|
299
336
|
path = path.replace(f"{{{param_name}}}", str(param_value))
|
|
300
337
|
|
|
338
|
+
# Filter any query parameters - get query parameters and filter out None/empty values
|
|
339
|
+
query_params = {}
|
|
340
|
+
for param in self._route.parameters:
|
|
341
|
+
if param.location == "query" and hasattr(self, f"_{param.name}"):
|
|
342
|
+
value = getattr(self, f"_{param.name}")
|
|
343
|
+
if value is not None and value != "":
|
|
344
|
+
query_params[param.name] = value
|
|
345
|
+
|
|
301
346
|
response = await self._client.request(
|
|
302
347
|
method=self._route.method,
|
|
303
348
|
url=path,
|
|
304
|
-
|
|
349
|
+
params=query_params,
|
|
350
|
+
timeout=self._timeout,
|
|
305
351
|
)
|
|
306
352
|
|
|
307
353
|
# Raise for 4xx/5xx responses
|
|
@@ -349,6 +395,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
349
395
|
description: str,
|
|
350
396
|
parameters: dict[str, Any],
|
|
351
397
|
tags: set[str] = set(),
|
|
398
|
+
timeout: float | None = None,
|
|
352
399
|
):
|
|
353
400
|
super().__init__(
|
|
354
401
|
uri_template=uri_template,
|
|
@@ -361,6 +408,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
361
408
|
)
|
|
362
409
|
self._client = client
|
|
363
410
|
self._route = route
|
|
411
|
+
self._timeout = timeout
|
|
364
412
|
|
|
365
413
|
async def create_resource(
|
|
366
414
|
self,
|
|
@@ -383,6 +431,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
383
431
|
description=self.description or f"Resource for {self._route.path}",
|
|
384
432
|
mime_type="application/json",
|
|
385
433
|
tags=set(self._route.tags or []),
|
|
434
|
+
timeout=self._timeout,
|
|
386
435
|
)
|
|
387
436
|
|
|
388
437
|
|
|
@@ -430,6 +479,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
430
479
|
client: httpx.AsyncClient,
|
|
431
480
|
name: str | None = None,
|
|
432
481
|
route_maps: list[RouteMap] | None = None,
|
|
482
|
+
timeout: float | None = None,
|
|
433
483
|
**settings: Any,
|
|
434
484
|
):
|
|
435
485
|
"""
|
|
@@ -440,13 +490,13 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
440
490
|
client: httpx AsyncClient for making HTTP requests
|
|
441
491
|
name: Optional name for the server
|
|
442
492
|
route_maps: Optional list of RouteMap objects defining route mappings
|
|
443
|
-
|
|
493
|
+
timeout: Optional timeout (in seconds) for all requests
|
|
444
494
|
**settings: Additional settings for FastMCP
|
|
445
495
|
"""
|
|
446
496
|
super().__init__(name=name or "OpenAPI FastMCP", **settings)
|
|
447
497
|
|
|
448
498
|
self._client = client
|
|
449
|
-
|
|
499
|
+
self._timeout = timeout
|
|
450
500
|
http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
|
|
451
501
|
|
|
452
502
|
# Process routes
|
|
@@ -489,10 +539,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
489
539
|
or f"Executes {route.method} {route.path}"
|
|
490
540
|
)
|
|
491
541
|
|
|
492
|
-
# Format enhanced description
|
|
542
|
+
# Format enhanced description with parameters and request body
|
|
493
543
|
enhanced_description = format_description_with_responses(
|
|
494
544
|
base_description=base_description,
|
|
495
545
|
responses=route.responses,
|
|
546
|
+
parameters=route.parameters,
|
|
547
|
+
request_body=route.request_body,
|
|
496
548
|
)
|
|
497
549
|
|
|
498
550
|
tool = OpenAPITool(
|
|
@@ -504,6 +556,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
504
556
|
fn_metadata=func_metadata(_openapi_passthrough),
|
|
505
557
|
is_async=True,
|
|
506
558
|
tags=set(route.tags or []),
|
|
559
|
+
timeout=self._timeout,
|
|
507
560
|
)
|
|
508
561
|
# Register the tool by directly assigning to the tools dictionary
|
|
509
562
|
self._tool_manager._tools[tool_name] = tool
|
|
@@ -519,10 +572,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
519
572
|
route.description or route.summary or f"Represents {route.path}"
|
|
520
573
|
)
|
|
521
574
|
|
|
522
|
-
# Format enhanced description
|
|
575
|
+
# Format enhanced description with parameters and request body
|
|
523
576
|
enhanced_description = format_description_with_responses(
|
|
524
577
|
base_description=base_description,
|
|
525
578
|
responses=route.responses,
|
|
579
|
+
parameters=route.parameters,
|
|
580
|
+
request_body=route.request_body,
|
|
526
581
|
)
|
|
527
582
|
|
|
528
583
|
resource = OpenAPIResource(
|
|
@@ -532,6 +587,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
532
587
|
name=resource_name,
|
|
533
588
|
description=enhanced_description,
|
|
534
589
|
tags=set(route.tags or []),
|
|
590
|
+
timeout=self._timeout,
|
|
535
591
|
)
|
|
536
592
|
# Register the resource by directly assigning to the resources dictionary
|
|
537
593
|
self._resource_manager._resources[str(resource.uri)] = resource
|
|
@@ -553,16 +609,30 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
553
609
|
route.description or route.summary or f"Template for {route.path}"
|
|
554
610
|
)
|
|
555
611
|
|
|
556
|
-
# Format enhanced description
|
|
612
|
+
# Format enhanced description with parameters and request body
|
|
557
613
|
enhanced_description = format_description_with_responses(
|
|
558
614
|
base_description=base_description,
|
|
559
615
|
responses=route.responses,
|
|
616
|
+
parameters=route.parameters,
|
|
617
|
+
request_body=route.request_body,
|
|
560
618
|
)
|
|
561
619
|
|
|
562
620
|
template_params_schema = {
|
|
563
621
|
"type": "object",
|
|
564
622
|
"properties": {
|
|
565
|
-
p.name:
|
|
623
|
+
p.name: {
|
|
624
|
+
**(p.schema_.copy() if isinstance(p.schema_, dict) else {}),
|
|
625
|
+
**(
|
|
626
|
+
{"description": p.description}
|
|
627
|
+
if p.description
|
|
628
|
+
and not (
|
|
629
|
+
isinstance(p.schema_, dict) and "description" in p.schema_
|
|
630
|
+
)
|
|
631
|
+
else {}
|
|
632
|
+
),
|
|
633
|
+
}
|
|
634
|
+
for p in route.parameters
|
|
635
|
+
if p.location == "path"
|
|
566
636
|
},
|
|
567
637
|
"required": [
|
|
568
638
|
p.name for p in route.parameters if p.location == "path" and p.required
|
|
@@ -577,6 +647,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
577
647
|
description=enhanced_description,
|
|
578
648
|
parameters=template_params_schema,
|
|
579
649
|
tags=set(route.tags or []),
|
|
650
|
+
timeout=self._timeout,
|
|
580
651
|
)
|
|
581
652
|
# Register the template by directly assigning to the templates dictionary
|
|
582
653
|
self._resource_manager._templates[uri_template_str] = template
|
|
@@ -589,13 +660,4 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
589
660
|
|
|
590
661
|
context = self.get_context()
|
|
591
662
|
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
|
592
|
-
|
|
593
|
-
# For other tools, ensure the response is wrapped in TextContent
|
|
594
|
-
if isinstance(result, dict | str):
|
|
595
|
-
if isinstance(result, dict):
|
|
596
|
-
result_text = json.dumps(result)
|
|
597
|
-
else:
|
|
598
|
-
result_text = result
|
|
599
|
-
return [TextContent(text=result_text, type="text")]
|
|
600
|
-
|
|
601
663
|
return result
|
fastmcp/server/proxy.py
CHANGED
|
@@ -61,12 +61,13 @@ class ProxyTool(Tool):
|
|
|
61
61
|
self,
|
|
62
62
|
arguments: dict[str, Any],
|
|
63
63
|
context: Context[ServerSessionT, LifespanContextT] | None = None,
|
|
64
|
-
) ->
|
|
64
|
+
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
65
65
|
# the client context manager will swallow any exceptions inside a TaskGroup
|
|
66
66
|
# so we return the raw result and raise an exception ourselves
|
|
67
67
|
async with self._client:
|
|
68
|
-
result = await self._client.
|
|
69
|
-
self.name,
|
|
68
|
+
result = await self._client.call_tool_mcp(
|
|
69
|
+
name=self.name,
|
|
70
|
+
arguments=arguments,
|
|
70
71
|
)
|
|
71
72
|
if result.isError:
|
|
72
73
|
raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
|