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/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" and p.name in kwargs
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=30.0, # Default 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(self, arguments: dict[str, Any], context: Any = None) -> Any:
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
- return await self._execute_request(**arguments, context=context)
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
- # Extract parameters from the URI
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
- # Assume the last parameter in the URI is for the first path parameter in the route
294
- path_param_name = param_matches[0]
295
- path_params[path_param_name] = param_value
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
- timeout=30.0, # Default timeout
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
- default_mime_type: Default MIME type for resources
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: p.schema_ for p in route.parameters if p.location == "path"
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
- ) -> Any:
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.call_tool(
69
- self.name, arguments, _return_raw_result=True
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)