fastmcp 2.3.5__py3-none-any.whl → 2.5.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/server/openapi.py CHANGED
@@ -5,8 +5,10 @@ from __future__ import annotations
5
5
  import enum
6
6
  import json
7
7
  import re
8
+ import warnings
9
+ from collections import Counter
8
10
  from collections.abc import Callable
9
- from dataclasses import dataclass
11
+ from dataclasses import dataclass, field
10
12
  from re import Pattern
11
13
  from typing import TYPE_CHECKING, Any, Literal
12
14
 
@@ -16,11 +18,13 @@ from pydantic.networks import AnyUrl
16
18
 
17
19
  from fastmcp.exceptions import ToolError
18
20
  from fastmcp.resources import Resource, ResourceTemplate
21
+ from fastmcp.server.dependencies import get_http_request
19
22
  from fastmcp.server.server import FastMCP
20
23
  from fastmcp.tools.tool import Tool, _convert_to_content
21
24
  from fastmcp.utilities import openapi
22
25
  from fastmcp.utilities.logging import get_logger
23
26
  from fastmcp.utilities.openapi import (
27
+ HTTPRoute,
24
28
  _combine_schemas,
25
29
  format_description_with_responses,
26
30
  )
@@ -33,13 +37,88 @@ logger = get_logger(__name__)
33
37
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
34
38
 
35
39
 
40
+ def _slugify(text: str) -> str:
41
+ """
42
+ Convert text to a URL-friendly slug format that only contains lowercase
43
+ letters, uppercase letters, numbers, and underscores.
44
+ """
45
+ if not text:
46
+ return ""
47
+
48
+ # Replace spaces and common separators with underscores
49
+ slug = re.sub(r"[\s\-\.]+", "_", text)
50
+
51
+ # Remove non-alphanumeric characters except underscores
52
+ slug = re.sub(r"[^a-zA-Z0-9_]", "", slug)
53
+
54
+ # Remove multiple consecutive underscores
55
+ slug = re.sub(r"_+", "_", slug)
56
+
57
+ # Remove leading/trailing underscores
58
+ slug = slug.strip("_")
59
+
60
+ return slug
61
+
62
+
63
+ def _get_mcp_client_headers() -> dict[str, str]:
64
+ """
65
+ Extract headers from the current MCP client HTTP request if available.
66
+
67
+ These headers will take precedence over OpenAPI-defined headers when both are present.
68
+
69
+ Returns:
70
+ Dictionary of header name-value pairs (lowercased names), or empty dict if no HTTP request is active.
71
+ """
72
+ try:
73
+ http_request = get_http_request()
74
+ return {
75
+ name.lower(): str(value) for name, value in http_request.headers.items()
76
+ }
77
+ except RuntimeError:
78
+ # No active HTTP request (e.g., STDIO transport), return empty dict
79
+ return {}
80
+
81
+
82
+ # Type definitions for the mapping functions
83
+ RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
84
+ ComponentFn = Callable[
85
+ [
86
+ HTTPRoute,
87
+ "OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate",
88
+ ],
89
+ None,
90
+ ]
91
+
92
+
93
+ class MCPType(enum.Enum):
94
+ """Type of FastMCP component to create from a route.
95
+
96
+ Enum values:
97
+ TOOL: Convert the route to a callable Tool
98
+ RESOURCE: Convert the route to a Resource (typically GET endpoints)
99
+ RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)
100
+ EXCLUDE: Exclude the route from being converted to any MCP component
101
+ IGNORE: Deprecated, use EXCLUDE instead
102
+ """
103
+
104
+ TOOL = "TOOL"
105
+ RESOURCE = "RESOURCE"
106
+ RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
107
+ # PROMPT = "PROMPT"
108
+ EXCLUDE = "EXCLUDE"
109
+
110
+
111
+ # Keep RouteType as an alias to MCPType for backward compatibility
36
112
  class RouteType(enum.Enum):
37
- """Type of FastMCP component to create from a route."""
113
+ """
114
+ Deprecated: Use MCPType instead.
115
+
116
+ This enum is kept for backward compatibility and will be removed in a future version.
117
+ """
38
118
 
39
119
  TOOL = "TOOL"
40
120
  RESOURCE = "RESOURCE"
41
121
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
42
- PROMPT = "PROMPT"
43
122
  IGNORE = "IGNORE"
44
123
 
45
124
 
@@ -47,32 +126,71 @@ class RouteType(enum.Enum):
47
126
  class RouteMap:
48
127
  """Mapping configuration for HTTP routes to FastMCP component types."""
49
128
 
50
- methods: list[HttpMethod]
51
- pattern: Pattern[str] | str
52
- route_type: RouteType
129
+ methods: list[HttpMethod] | Literal["*"] = field(default="*")
130
+ pattern: Pattern[str] | str = field(default=r".*")
131
+ mcp_type: MCPType | None = field(default=None)
132
+ route_type: RouteType | MCPType | None = field(default=None)
133
+ tags: set[str] = field(default_factory=set)
134
+
135
+ def __post_init__(self):
136
+ """Validate and process the route map after initialization."""
137
+ # Handle backward compatibility for route_type, deprecated in 2.5.0
138
+ if self.mcp_type is None and self.route_type is not None:
139
+ warnings.warn(
140
+ "The 'route_type' parameter is deprecated and will be removed in a future version. "
141
+ "Use 'mcp_type' instead with the appropriate MCPType value.",
142
+ DeprecationWarning,
143
+ stacklevel=2,
144
+ )
145
+ if isinstance(self.route_type, RouteType):
146
+ warnings.warn(
147
+ "The RouteType class is deprecated and will be removed in a future version. "
148
+ "Use MCPType instead.",
149
+ DeprecationWarning,
150
+ stacklevel=2,
151
+ )
152
+ # Check for the deprecated IGNORE value
153
+ if self.route_type == RouteType.IGNORE:
154
+ warnings.warn(
155
+ "RouteType.IGNORE is deprecated and will be removed in a future version. "
156
+ "Use MCPType.EXCLUDE instead.",
157
+ DeprecationWarning,
158
+ stacklevel=2,
159
+ )
160
+
161
+ # Convert from RouteType to MCPType if needed
162
+ if isinstance(self.route_type, RouteType):
163
+ route_type_name = self.route_type.name
164
+ if route_type_name == "IGNORE":
165
+ route_type_name = "EXCLUDE"
166
+ self.mcp_type = getattr(MCPType, route_type_name)
167
+ else:
168
+ self.mcp_type = self.route_type
169
+ elif self.mcp_type is None:
170
+ raise ValueError("`mcp_type` must be provided")
171
+
172
+ # Set route_type to match mcp_type for backward compatibility
173
+ if self.route_type is None:
174
+ self.route_type = self.mcp_type
53
175
 
54
176
 
55
177
  # Default route mappings as a list, where order determines priority
56
178
  DEFAULT_ROUTE_MAPPINGS = [
57
179
  # GET requests with path parameters go to ResourceTemplate
58
180
  RouteMap(
59
- methods=["GET"], pattern=r".*\{.*\}.*", route_type=RouteType.RESOURCE_TEMPLATE
181
+ methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE
60
182
  ),
61
183
  # GET requests without path parameters go to Resource
62
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
184
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
63
185
  # All other HTTP methods go to Tool
64
- RouteMap(
65
- methods=["POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
66
- pattern=r".*",
67
- route_type=RouteType.TOOL,
68
- ),
186
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL),
69
187
  ]
70
188
 
71
189
 
72
190
  def _determine_route_type(
73
191
  route: openapi.HTTPRoute,
74
192
  mappings: list[RouteMap],
75
- ) -> RouteType:
193
+ ) -> MCPType:
76
194
  """
77
195
  Determines the FastMCP component type based on the route and mappings.
78
196
 
@@ -81,12 +199,12 @@ def _determine_route_type(
81
199
  mappings: List of RouteMap objects in priority order
82
200
 
83
201
  Returns:
84
- RouteType for this route
202
+ MCPType for this route
85
203
  """
86
204
  # Check mappings in priority order (first match wins)
87
205
  for route_map in mappings:
88
206
  # Check if the HTTP method matches
89
- if route.method in route_map.methods:
207
+ if route_map.methods == "*" or route.method in route_map.methods:
90
208
  # Handle both string patterns and compiled Pattern objects
91
209
  if isinstance(route_map.pattern, Pattern):
92
210
  pattern_matches = route_map.pattern.search(route.path)
@@ -94,20 +212,24 @@ def _determine_route_type(
94
212
  pattern_matches = re.search(route_map.pattern, route.path)
95
213
 
96
214
  if pattern_matches:
215
+ # Check if tags match (if specified)
216
+ # If route_map.tags is empty, tags are not matched
217
+ # If route_map.tags is non-empty, all tags must be present in route.tags (AND condition)
218
+ if route_map.tags:
219
+ route_tags_set = set(route.tags or [])
220
+ if not route_map.tags.issubset(route_tags_set):
221
+ # Tags don't match, continue to next mapping
222
+ continue
223
+
224
+ # We know mcp_type is not None here due to post_init validation
225
+ assert route_map.mcp_type is not None
97
226
  logger.debug(
98
- f"Route {route.method} {route.path} matched mapping to {route_map.route_type.name}"
227
+ f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
99
228
  )
100
- return route_map.route_type
229
+ return route_map.mcp_type
101
230
 
102
231
  # Default fallback
103
- return RouteType.TOOL
104
-
105
-
106
- # Placeholder function to provide function metadata
107
- async def _openapi_passthrough(*args, **kwargs):
108
- """Placeholder function for OpenAPI endpoints."""
109
- # This is kept for metadata generation purposes
110
- pass
232
+ return MCPType.TOOL
111
233
 
112
234
 
113
235
  class OpenAPITool(Tool):
@@ -171,27 +293,138 @@ class OpenAPITool(Tool):
171
293
  raise ToolError(f"Missing required path parameters: {missing_params}")
172
294
 
173
295
  for param_name, param_value in path_params.items():
296
+ # Handle array path parameters with style 'simple' (comma-separated)
297
+ # In OpenAPI, 'simple' is the default style for path parameters
298
+ param_info = next(
299
+ (p for p in self._route.parameters if p.name == param_name), None
300
+ )
301
+
302
+ if param_info and isinstance(param_value, list):
303
+ # Check if schema indicates an array type
304
+ schema = param_info.schema_
305
+ is_array = schema.get("type") == "array"
306
+
307
+ if is_array:
308
+ # Format array values as comma-separated string
309
+ # This follows the OpenAPI 'simple' style (default for path)
310
+ if all(
311
+ isinstance(item, str | int | float | bool)
312
+ for item in param_value
313
+ ):
314
+ # Handle simple array types
315
+ path = path.replace(
316
+ f"{{{param_name}}}", ",".join(str(v) for v in param_value)
317
+ )
318
+ else:
319
+ # Handle complex array types (containing objects/dicts)
320
+ try:
321
+ # Try to create a simple representation without Python syntax artifacts
322
+ formatted_parts = []
323
+ for item in param_value:
324
+ if isinstance(item, dict):
325
+ # For objects, serialize key-value pairs
326
+ item_parts = []
327
+ for k, v in item.items():
328
+ item_parts.append(f"{k}:{v}")
329
+ formatted_parts.append(".".join(item_parts))
330
+ else:
331
+ # Fallback for other complex types
332
+ formatted_parts.append(str(item))
333
+
334
+ # Join parts with commas
335
+ formatted_value = ",".join(formatted_parts)
336
+ path = path.replace(f"{{{param_name}}}", formatted_value)
337
+ except Exception as e:
338
+ logger.warning(
339
+ f"Failed to format complex array path parameter '{param_name}': {e}"
340
+ )
341
+ # Fallback to string representation, but remove Python syntax artifacts
342
+ str_value = (
343
+ str(param_value)
344
+ .replace("[", "")
345
+ .replace("]", "")
346
+ .replace("'", "")
347
+ .replace('"', "")
348
+ )
349
+ path = path.replace(f"{{{param_name}}}", str_value)
350
+ continue
351
+
352
+ # Default handling for non-array parameters or non-array schemas
174
353
  path = path.replace(f"{{{param_name}}}", str(param_value))
175
354
 
176
355
  # Prepare query parameters - filter out None and empty strings
177
- query_params = {
178
- p.name: kwargs.get(p.name)
179
- for p in self._route.parameters
180
- if p.location == "query"
181
- and p.name in kwargs
182
- and kwargs.get(p.name) is not None
183
- and kwargs.get(p.name) != ""
184
- }
356
+ query_params = {}
357
+ for p in self._route.parameters:
358
+ if (
359
+ p.location == "query"
360
+ and p.name in kwargs
361
+ and kwargs.get(p.name) is not None
362
+ and kwargs.get(p.name) != ""
363
+ ):
364
+ param_value = kwargs.get(p.name)
365
+
366
+ # Format array query parameters as comma-separated strings
367
+ # following OpenAPI form style (default for query parameters)
368
+ if isinstance(param_value, list) and p.schema_.get("type") == "array":
369
+ # Get explode parameter from schema, default is True for query parameters
370
+ # If explode is True, the array is serialized as separate parameters
371
+ # If explode is False, the array is serialized as a comma-separated string
372
+ explode = p.schema_.get("explode", True)
373
+
374
+ if explode:
375
+ # When explode=True, we pass the array directly, which HTTPX will serialize
376
+ # as multiple parameters with the same name
377
+ query_params[p.name] = param_value
378
+ else:
379
+ # For arrays of simple types (strings, numbers, etc.), join with commas
380
+ if all(
381
+ isinstance(item, str | int | float | bool)
382
+ for item in param_value
383
+ ):
384
+ query_params[p.name] = ",".join(str(v) for v in param_value)
385
+ else:
386
+ # For complex types, try to create a simpler representation
387
+ try:
388
+ # Try to create a simple string representation
389
+ formatted_parts = []
390
+ for item in param_value:
391
+ if isinstance(item, dict):
392
+ # For objects, serialize key-value pairs
393
+ item_parts = []
394
+ for k, v in item.items():
395
+ item_parts.append(f"{k}:{v}")
396
+ formatted_parts.append(".".join(item_parts))
397
+ else:
398
+ formatted_parts.append(str(item))
399
+
400
+ query_params[p.name] = ",".join(formatted_parts)
401
+ except Exception as e:
402
+ logger.warning(
403
+ f"Failed to format complex array query parameter '{p.name}': {e}"
404
+ )
405
+ # Fallback to string representation
406
+ query_params[p.name] = param_value
407
+ else:
408
+ # Non-array parameters are passed as is
409
+ query_params[p.name] = param_value
185
410
 
186
411
  # Prepare headers - fix typing by ensuring all values are strings
187
412
  headers = {}
413
+
414
+ # Start with OpenAPI-defined header parameters
415
+ openapi_headers = {}
188
416
  for p in self._route.parameters:
189
417
  if (
190
418
  p.location == "header"
191
419
  and p.name in kwargs
192
420
  and kwargs[p.name] is not None
193
421
  ):
194
- headers[p.name] = str(kwargs[p.name])
422
+ openapi_headers[p.name.lower()] = str(kwargs[p.name])
423
+ headers.update(openapi_headers)
424
+
425
+ # Add headers from the current MCP client HTTP request (these take precedence)
426
+ mcp_headers = _get_mcp_client_headers()
427
+ headers.update(mcp_headers)
195
428
 
196
429
  # Prepare request body
197
430
  json_data = None
@@ -339,10 +572,16 @@ class OpenAPIResource(Resource):
339
572
  if value is not None and value != "":
340
573
  query_params[param.name] = value
341
574
 
575
+ # Prepare headers from MCP client request if available
576
+ headers = {}
577
+ mcp_headers = _get_mcp_client_headers()
578
+ headers.update(mcp_headers)
579
+
342
580
  response = await self._client.request(
343
581
  method=self._route.method,
344
582
  url=path,
345
583
  params=query_params,
584
+ headers=headers,
346
585
  timeout=self._timeout,
347
586
  )
348
587
 
@@ -443,7 +682,7 @@ class FastMCPOpenAPI(FastMCP):
443
682
 
444
683
  Example:
445
684
  ```python
446
- from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, RouteType
685
+ from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
447
686
  import httpx
448
687
 
449
688
  # Define custom route mappings
@@ -452,17 +691,17 @@ class FastMCPOpenAPI(FastMCP):
452
691
  RouteMap(
453
692
  methods=["GET", "POST", "PATCH"],
454
693
  pattern=r".*/users/.*",
455
- route_type=RouteType.RESOURCE_TEMPLATE
694
+ mcp_type=MCPType.RESOURCE_TEMPLATE
456
695
  ),
457
696
  # Map all analytics endpoints to Tool
458
697
  RouteMap(
459
698
  methods=["GET"],
460
699
  pattern=r".*/analytics/.*",
461
- route_type=RouteType.TOOL
700
+ mcp_type=MCPType.TOOL
462
701
  ),
463
702
  ]
464
703
 
465
- # Create server with custom mappings
704
+ # Create server with custom mappings and route mapper
466
705
  server = FastMCPOpenAPI(
467
706
  openapi_spec=spec,
468
707
  client=httpx.AsyncClient(),
@@ -478,6 +717,9 @@ class FastMCPOpenAPI(FastMCP):
478
717
  client: httpx.AsyncClient,
479
718
  name: str | None = None,
480
719
  route_maps: list[RouteMap] | None = None,
720
+ route_map_fn: RouteMapFn | None = None,
721
+ mcp_component_fn: ComponentFn | None = None,
722
+ mcp_names: dict[str, str] | None = None,
481
723
  timeout: float | None = None,
482
724
  **settings: Any,
483
725
  ):
@@ -489,6 +731,17 @@ class FastMCPOpenAPI(FastMCP):
489
731
  client: httpx AsyncClient for making HTTP requests
490
732
  name: Optional name for the server
491
733
  route_maps: Optional list of RouteMap objects defining route mappings
734
+ route_map_fn: Optional callable for advanced route type mapping.
735
+ Receives (route, mcp_type) and returns MCPType or None.
736
+ Called on every route, including excluded ones.
737
+ mcp_component_fn: Optional callable for component customization.
738
+ Receives (route, component) and can modify the component in-place.
739
+ Called on every created component.
740
+ mcp_names: Optional dictionary mapping operationId to desired component names.
741
+ If an operationId is not in the dictionary, falls back to using the
742
+ operationId up to the first double underscore. If no operationId exists,
743
+ falls back to slugified summary or path-based naming.
744
+ All names are truncated to 56 characters maximum.
492
745
  timeout: Optional timeout (in seconds) for all requests
493
746
  **settings: Additional settings for FastMCP
494
747
  """
@@ -496,6 +749,18 @@ class FastMCPOpenAPI(FastMCP):
496
749
 
497
750
  self._client = client
498
751
  self._timeout = timeout
752
+ self._route_map_fn = route_map_fn
753
+ self._mcp_component_fn = mcp_component_fn
754
+ self._mcp_names = mcp_names or {}
755
+
756
+ # Keep track of names to detect collisions
757
+ self._used_names = {
758
+ "tool": Counter(),
759
+ "resource": Counter(),
760
+ "resource_template": Counter(),
761
+ "prompt": Counter(),
762
+ }
763
+
499
764
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
500
765
 
501
766
  # Process routes
@@ -504,34 +769,97 @@ class FastMCPOpenAPI(FastMCP):
504
769
  # Determine route type based on mappings or default rules
505
770
  route_type = _determine_route_type(route, route_maps)
506
771
 
507
- # Use operation_id if available, otherwise generate a name
508
- operation_id = route.operation_id
509
- if not operation_id:
510
- # Generate operation ID from method and path
511
- path_parts = route.path.strip("/").split("/")
512
- path_name = "_".join(p for p in path_parts if not p.startswith("{"))
513
- operation_id = f"{route.method.lower()}_{path_name}"
514
-
515
- if route_type == RouteType.TOOL:
516
- self._create_openapi_tool(route, operation_id)
517
- elif route_type == RouteType.RESOURCE:
518
- self._create_openapi_resource(route, operation_id)
519
- elif route_type == RouteType.RESOURCE_TEMPLATE:
520
- self._create_openapi_template(route, operation_id)
521
- elif route_type == RouteType.PROMPT:
522
- # Not implemented yet
523
- logger.warning(
524
- f"PROMPT route type not implemented: {route.method} {route.path}"
525
- )
526
- elif route_type == RouteType.IGNORE:
527
- logger.info(f"Ignoring route: {route.method} {route.path}")
772
+ # Call route_map_fn if provided
773
+ if self._route_map_fn is not None:
774
+ try:
775
+ result = self._route_map_fn(route, route_type)
776
+ if result is not None:
777
+ route_type = result
778
+ logger.debug(
779
+ f"Route {route.method} {route.path} mapping customized by route_map_fn: "
780
+ f"type={route_type.name}"
781
+ )
782
+ except Exception as e:
783
+ logger.warning(
784
+ f"Error in route_map_fn for {route.method} {route.path}: {e}. "
785
+ f"Using default values."
786
+ )
787
+
788
+ # Generate a default name from the route
789
+ component_name = self._generate_default_name(route, route_type)
790
+
791
+ if route_type == MCPType.TOOL:
792
+ self._create_openapi_tool(route, component_name)
793
+ elif route_type == MCPType.RESOURCE:
794
+ self._create_openapi_resource(route, component_name)
795
+ elif route_type == MCPType.RESOURCE_TEMPLATE:
796
+ self._create_openapi_template(route, component_name)
797
+ elif route_type == MCPType.EXCLUDE:
798
+ logger.info(f"Excluding route: {route.method} {route.path}")
528
799
 
529
800
  logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
530
801
 
531
- def _create_openapi_tool(self, route: openapi.HTTPRoute, operation_id: str):
802
+ def _generate_default_name(
803
+ self, route: openapi.HTTPRoute, mcp_type: MCPType
804
+ ) -> str:
805
+ """Generate a default name from the route using the configured strategy."""
806
+ name = ""
807
+
808
+ # First check if there's a custom mapping for this operationId
809
+ if route.operation_id:
810
+ if route.operation_id in self._mcp_names:
811
+ name = self._mcp_names[route.operation_id]
812
+ else:
813
+ # If there's a double underscore in the operationId, use the first part
814
+ name = route.operation_id.split("__")[0]
815
+ else:
816
+ name = route.summary or f"{route.method}_{route.path}"
817
+
818
+ name = _slugify(name)
819
+
820
+ # Truncate to 56 characters maximum
821
+ if len(name) > 56:
822
+ name = name[:56]
823
+
824
+ return name
825
+
826
+ def _get_unique_name(
827
+ self,
828
+ name: str,
829
+ component_type: Literal["tool", "resource", "resource_template", "prompt"],
830
+ ) -> str:
831
+ """
832
+ Ensure the name is unique within its component type by appending numbers if needed.
833
+
834
+ Args:
835
+ name: The proposed name
836
+ component_type: The type of component ("tools", "resources", or "templates")
837
+
838
+ Returns:
839
+ str: A unique name for the component
840
+ """
841
+ # Check if the name is already used
842
+ self._used_names[component_type][name] += 1
843
+ if self._used_names[component_type][name] == 1:
844
+ return name
845
+
846
+ else:
847
+ # Create the new name
848
+ new_name = f"{name}_{self._used_names[component_type][name]}"
849
+ logger.debug(
850
+ f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
851
+ f"Using '{new_name}' instead."
852
+ )
853
+
854
+ return new_name
855
+
856
+ def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
532
857
  """Creates and registers an OpenAPITool with enhanced description."""
533
858
  combined_schema = _combine_schemas(route)
534
- tool_name = operation_id
859
+
860
+ # Get a unique tool name
861
+ tool_name = self._get_unique_name(name, "tool")
862
+
535
863
  base_description = (
536
864
  route.description
537
865
  or route.summary
@@ -555,16 +883,30 @@ class FastMCPOpenAPI(FastMCP):
555
883
  tags=set(route.tags or []),
556
884
  timeout=self._timeout,
557
885
  )
886
+
887
+ # Call component_fn if provided
888
+ if self._mcp_component_fn is not None:
889
+ try:
890
+ self._mcp_component_fn(route, tool)
891
+ logger.debug(f"Tool {tool_name} customized by component_fn")
892
+ except Exception as e:
893
+ logger.warning(
894
+ f"Error in component_fn for tool {tool_name}: {e}. "
895
+ f"Using component as-is."
896
+ )
897
+
558
898
  # Register the tool by directly assigning to the tools dictionary
559
899
  self._tool_manager._tools[tool_name] = tool
560
900
  logger.debug(
561
901
  f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
562
902
  )
563
903
 
564
- def _create_openapi_resource(self, route: openapi.HTTPRoute, operation_id: str):
904
+ def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
565
905
  """Creates and registers an OpenAPIResource with enhanced description."""
566
- resource_name = operation_id
567
- resource_uri = f"resource://openapi/{resource_name}"
906
+ # Get a unique resource name
907
+ resource_name = self._get_unique_name(name, "resource")
908
+
909
+ resource_uri = f"resource://{resource_name}"
568
910
  base_description = (
569
911
  route.description or route.summary or f"Represents {route.path}"
570
912
  )
@@ -586,19 +928,33 @@ class FastMCPOpenAPI(FastMCP):
586
928
  tags=set(route.tags or []),
587
929
  timeout=self._timeout,
588
930
  )
931
+
932
+ # Call component_fn if provided
933
+ if self._mcp_component_fn is not None:
934
+ try:
935
+ self._mcp_component_fn(route, resource)
936
+ logger.debug(f"Resource {resource_uri} customized by component_fn")
937
+ except Exception as e:
938
+ logger.warning(
939
+ f"Error in component_fn for resource {resource_uri}: {e}. "
940
+ f"Using component as-is."
941
+ )
942
+
589
943
  # Register the resource by directly assigning to the resources dictionary
590
944
  self._resource_manager._resources[str(resource.uri)] = resource
591
945
  logger.debug(
592
946
  f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
593
947
  )
594
948
 
595
- def _create_openapi_template(self, route: openapi.HTTPRoute, operation_id: str):
949
+ def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
596
950
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
597
- template_name = operation_id
951
+ # Get a unique template name
952
+ template_name = self._get_unique_name(name, "resource_template")
953
+
598
954
  path_params = [p.name for p in route.parameters if p.location == "path"]
599
955
  path_params.sort() # Sort for consistent URIs
600
956
 
601
- uri_template_str = f"resource://openapi/{template_name}"
957
+ uri_template_str = f"resource://{template_name}"
602
958
  if path_params:
603
959
  uri_template_str += "/" + "/".join(f"{{{p}}}" for p in path_params)
604
960
 
@@ -646,13 +1002,20 @@ class FastMCPOpenAPI(FastMCP):
646
1002
  tags=set(route.tags or []),
647
1003
  timeout=self._timeout,
648
1004
  )
1005
+
1006
+ # Call component_fn if provided
1007
+ if self._mcp_component_fn is not None:
1008
+ try:
1009
+ self._mcp_component_fn(route, template)
1010
+ logger.debug(f"Template {uri_template_str} customized by component_fn")
1011
+ except Exception as e:
1012
+ logger.warning(
1013
+ f"Error in component_fn for template {uri_template_str}: {e}. "
1014
+ f"Using component as-is."
1015
+ )
1016
+
649
1017
  # Register the template by directly assigning to the templates dictionary
650
1018
  self._resource_manager._templates[uri_template_str] = template
651
1019
  logger.debug(
652
1020
  f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path}) with tags: {route.tags}"
653
1021
  )
654
-
655
- async def _mcp_call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
656
- """Override the call_tool method to return the raw result without converting to content."""
657
- result = await self._tool_manager.call_tool(name, arguments)
658
- return result