fastmcp 2.4.0__py3-none-any.whl → 2.5.1__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/client/client.py +23 -1
- fastmcp/client/transports.py +39 -18
- fastmcp/prompts/prompt.py +11 -4
- fastmcp/prompts/prompt_manager.py +25 -5
- fastmcp/resources/resource_manager.py +31 -5
- fastmcp/resources/template.py +10 -5
- fastmcp/server/context.py +46 -0
- fastmcp/server/dependencies.py +32 -0
- fastmcp/server/http.py +2 -0
- fastmcp/server/openapi.py +305 -64
- fastmcp/server/server.py +101 -49
- fastmcp/settings.py +30 -1
- fastmcp/tools/tool.py +5 -1
- fastmcp/tools/tool_manager.py +9 -2
- fastmcp/utilities/logging.py +6 -1
- fastmcp/utilities/mcp_config.py +4 -3
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.1.dist-info}/METADATA +4 -4
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.1.dist-info}/RECORD +21 -21
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.1.dist-info}/licenses/LICENSE +0 -0
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_headers
|
|
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,69 @@ 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
|
+
# Type definitions for the mapping functions
|
|
64
|
+
RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
|
|
65
|
+
ComponentFn = Callable[
|
|
66
|
+
[
|
|
67
|
+
HTTPRoute,
|
|
68
|
+
"OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate",
|
|
69
|
+
],
|
|
70
|
+
None,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MCPType(enum.Enum):
|
|
75
|
+
"""Type of FastMCP component to create from a route.
|
|
76
|
+
|
|
77
|
+
Enum values:
|
|
78
|
+
TOOL: Convert the route to a callable Tool
|
|
79
|
+
RESOURCE: Convert the route to a Resource (typically GET endpoints)
|
|
80
|
+
RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)
|
|
81
|
+
EXCLUDE: Exclude the route from being converted to any MCP component
|
|
82
|
+
IGNORE: Deprecated, use EXCLUDE instead
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
TOOL = "TOOL"
|
|
86
|
+
RESOURCE = "RESOURCE"
|
|
87
|
+
RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
|
|
88
|
+
# PROMPT = "PROMPT"
|
|
89
|
+
EXCLUDE = "EXCLUDE"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Keep RouteType as an alias to MCPType for backward compatibility
|
|
36
93
|
class RouteType(enum.Enum):
|
|
37
|
-
"""
|
|
94
|
+
"""
|
|
95
|
+
Deprecated: Use MCPType instead.
|
|
96
|
+
|
|
97
|
+
This enum is kept for backward compatibility and will be removed in a future version.
|
|
98
|
+
"""
|
|
38
99
|
|
|
39
100
|
TOOL = "TOOL"
|
|
40
101
|
RESOURCE = "RESOURCE"
|
|
41
102
|
RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
|
|
42
|
-
PROMPT = "PROMPT"
|
|
43
103
|
IGNORE = "IGNORE"
|
|
44
104
|
|
|
45
105
|
|
|
@@ -47,32 +107,71 @@ class RouteType(enum.Enum):
|
|
|
47
107
|
class RouteMap:
|
|
48
108
|
"""Mapping configuration for HTTP routes to FastMCP component types."""
|
|
49
109
|
|
|
50
|
-
methods: list[HttpMethod] | Literal["*"]
|
|
51
|
-
pattern: Pattern[str] | str
|
|
52
|
-
|
|
110
|
+
methods: list[HttpMethod] | Literal["*"] = field(default="*")
|
|
111
|
+
pattern: Pattern[str] | str = field(default=r".*")
|
|
112
|
+
mcp_type: MCPType | None = field(default=None)
|
|
113
|
+
route_type: RouteType | MCPType | None = field(default=None)
|
|
114
|
+
tags: set[str] = field(default_factory=set)
|
|
115
|
+
|
|
116
|
+
def __post_init__(self):
|
|
117
|
+
"""Validate and process the route map after initialization."""
|
|
118
|
+
# Handle backward compatibility for route_type, deprecated in 2.5.0
|
|
119
|
+
if self.mcp_type is None and self.route_type is not None:
|
|
120
|
+
warnings.warn(
|
|
121
|
+
"The 'route_type' parameter is deprecated and will be removed in a future version. "
|
|
122
|
+
"Use 'mcp_type' instead with the appropriate MCPType value.",
|
|
123
|
+
DeprecationWarning,
|
|
124
|
+
stacklevel=2,
|
|
125
|
+
)
|
|
126
|
+
if isinstance(self.route_type, RouteType):
|
|
127
|
+
warnings.warn(
|
|
128
|
+
"The RouteType class is deprecated and will be removed in a future version. "
|
|
129
|
+
"Use MCPType instead.",
|
|
130
|
+
DeprecationWarning,
|
|
131
|
+
stacklevel=2,
|
|
132
|
+
)
|
|
133
|
+
# Check for the deprecated IGNORE value
|
|
134
|
+
if self.route_type == RouteType.IGNORE:
|
|
135
|
+
warnings.warn(
|
|
136
|
+
"RouteType.IGNORE is deprecated and will be removed in a future version. "
|
|
137
|
+
"Use MCPType.EXCLUDE instead.",
|
|
138
|
+
DeprecationWarning,
|
|
139
|
+
stacklevel=2,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Convert from RouteType to MCPType if needed
|
|
143
|
+
if isinstance(self.route_type, RouteType):
|
|
144
|
+
route_type_name = self.route_type.name
|
|
145
|
+
if route_type_name == "IGNORE":
|
|
146
|
+
route_type_name = "EXCLUDE"
|
|
147
|
+
self.mcp_type = getattr(MCPType, route_type_name)
|
|
148
|
+
else:
|
|
149
|
+
self.mcp_type = self.route_type
|
|
150
|
+
elif self.mcp_type is None:
|
|
151
|
+
raise ValueError("`mcp_type` must be provided")
|
|
152
|
+
|
|
153
|
+
# Set route_type to match mcp_type for backward compatibility
|
|
154
|
+
if self.route_type is None:
|
|
155
|
+
self.route_type = self.mcp_type
|
|
53
156
|
|
|
54
157
|
|
|
55
158
|
# Default route mappings as a list, where order determines priority
|
|
56
159
|
DEFAULT_ROUTE_MAPPINGS = [
|
|
57
160
|
# GET requests with path parameters go to ResourceTemplate
|
|
58
161
|
RouteMap(
|
|
59
|
-
methods=["GET"], pattern=r".*\{.*\}.*",
|
|
162
|
+
methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE
|
|
60
163
|
),
|
|
61
164
|
# GET requests without path parameters go to Resource
|
|
62
|
-
RouteMap(methods=["GET"], pattern=r".*",
|
|
165
|
+
RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
|
|
63
166
|
# 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
|
-
),
|
|
167
|
+
RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL),
|
|
69
168
|
]
|
|
70
169
|
|
|
71
170
|
|
|
72
171
|
def _determine_route_type(
|
|
73
172
|
route: openapi.HTTPRoute,
|
|
74
173
|
mappings: list[RouteMap],
|
|
75
|
-
) ->
|
|
174
|
+
) -> MCPType:
|
|
76
175
|
"""
|
|
77
176
|
Determines the FastMCP component type based on the route and mappings.
|
|
78
177
|
|
|
@@ -81,7 +180,7 @@ def _determine_route_type(
|
|
|
81
180
|
mappings: List of RouteMap objects in priority order
|
|
82
181
|
|
|
83
182
|
Returns:
|
|
84
|
-
|
|
183
|
+
MCPType for this route
|
|
85
184
|
"""
|
|
86
185
|
# Check mappings in priority order (first match wins)
|
|
87
186
|
for route_map in mappings:
|
|
@@ -94,20 +193,24 @@ def _determine_route_type(
|
|
|
94
193
|
pattern_matches = re.search(route_map.pattern, route.path)
|
|
95
194
|
|
|
96
195
|
if pattern_matches:
|
|
196
|
+
# Check if tags match (if specified)
|
|
197
|
+
# If route_map.tags is empty, tags are not matched
|
|
198
|
+
# If route_map.tags is non-empty, all tags must be present in route.tags (AND condition)
|
|
199
|
+
if route_map.tags:
|
|
200
|
+
route_tags_set = set(route.tags or [])
|
|
201
|
+
if not route_map.tags.issubset(route_tags_set):
|
|
202
|
+
# Tags don't match, continue to next mapping
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# We know mcp_type is not None here due to post_init validation
|
|
206
|
+
assert route_map.mcp_type is not None
|
|
97
207
|
logger.debug(
|
|
98
|
-
f"Route {route.method} {route.path} matched mapping to {route_map.
|
|
208
|
+
f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
|
|
99
209
|
)
|
|
100
|
-
return route_map.
|
|
210
|
+
return route_map.mcp_type
|
|
101
211
|
|
|
102
212
|
# Default fallback
|
|
103
|
-
return
|
|
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
|
|
213
|
+
return MCPType.TOOL
|
|
111
214
|
|
|
112
215
|
|
|
113
216
|
class OpenAPITool(Tool):
|
|
@@ -288,13 +391,21 @@ class OpenAPITool(Tool):
|
|
|
288
391
|
|
|
289
392
|
# Prepare headers - fix typing by ensuring all values are strings
|
|
290
393
|
headers = {}
|
|
394
|
+
|
|
395
|
+
# Start with OpenAPI-defined header parameters
|
|
396
|
+
openapi_headers = {}
|
|
291
397
|
for p in self._route.parameters:
|
|
292
398
|
if (
|
|
293
399
|
p.location == "header"
|
|
294
400
|
and p.name in kwargs
|
|
295
401
|
and kwargs[p.name] is not None
|
|
296
402
|
):
|
|
297
|
-
|
|
403
|
+
openapi_headers[p.name.lower()] = str(kwargs[p.name])
|
|
404
|
+
headers.update(openapi_headers)
|
|
405
|
+
|
|
406
|
+
# Add headers from the current MCP client HTTP request (these take precedence)
|
|
407
|
+
mcp_headers = get_http_headers()
|
|
408
|
+
headers.update(mcp_headers)
|
|
298
409
|
|
|
299
410
|
# Prepare request body
|
|
300
411
|
json_data = None
|
|
@@ -442,10 +553,16 @@ class OpenAPIResource(Resource):
|
|
|
442
553
|
if value is not None and value != "":
|
|
443
554
|
query_params[param.name] = value
|
|
444
555
|
|
|
556
|
+
# Prepare headers from MCP client request if available
|
|
557
|
+
headers = {}
|
|
558
|
+
mcp_headers = get_http_headers()
|
|
559
|
+
headers.update(mcp_headers)
|
|
560
|
+
|
|
445
561
|
response = await self._client.request(
|
|
446
562
|
method=self._route.method,
|
|
447
563
|
url=path,
|
|
448
564
|
params=query_params,
|
|
565
|
+
headers=headers,
|
|
449
566
|
timeout=self._timeout,
|
|
450
567
|
)
|
|
451
568
|
|
|
@@ -546,7 +663,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
546
663
|
|
|
547
664
|
Example:
|
|
548
665
|
```python
|
|
549
|
-
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap,
|
|
666
|
+
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
|
|
550
667
|
import httpx
|
|
551
668
|
|
|
552
669
|
# Define custom route mappings
|
|
@@ -555,17 +672,17 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
555
672
|
RouteMap(
|
|
556
673
|
methods=["GET", "POST", "PATCH"],
|
|
557
674
|
pattern=r".*/users/.*",
|
|
558
|
-
|
|
675
|
+
mcp_type=MCPType.RESOURCE_TEMPLATE
|
|
559
676
|
),
|
|
560
677
|
# Map all analytics endpoints to Tool
|
|
561
678
|
RouteMap(
|
|
562
679
|
methods=["GET"],
|
|
563
680
|
pattern=r".*/analytics/.*",
|
|
564
|
-
|
|
681
|
+
mcp_type=MCPType.TOOL
|
|
565
682
|
),
|
|
566
683
|
]
|
|
567
684
|
|
|
568
|
-
# Create server with custom mappings
|
|
685
|
+
# Create server with custom mappings and route mapper
|
|
569
686
|
server = FastMCPOpenAPI(
|
|
570
687
|
openapi_spec=spec,
|
|
571
688
|
client=httpx.AsyncClient(),
|
|
@@ -581,6 +698,9 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
581
698
|
client: httpx.AsyncClient,
|
|
582
699
|
name: str | None = None,
|
|
583
700
|
route_maps: list[RouteMap] | None = None,
|
|
701
|
+
route_map_fn: RouteMapFn | None = None,
|
|
702
|
+
mcp_component_fn: ComponentFn | None = None,
|
|
703
|
+
mcp_names: dict[str, str] | None = None,
|
|
584
704
|
timeout: float | None = None,
|
|
585
705
|
**settings: Any,
|
|
586
706
|
):
|
|
@@ -592,6 +712,17 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
592
712
|
client: httpx AsyncClient for making HTTP requests
|
|
593
713
|
name: Optional name for the server
|
|
594
714
|
route_maps: Optional list of RouteMap objects defining route mappings
|
|
715
|
+
route_map_fn: Optional callable for advanced route type mapping.
|
|
716
|
+
Receives (route, mcp_type) and returns MCPType or None.
|
|
717
|
+
Called on every route, including excluded ones.
|
|
718
|
+
mcp_component_fn: Optional callable for component customization.
|
|
719
|
+
Receives (route, component) and can modify the component in-place.
|
|
720
|
+
Called on every created component.
|
|
721
|
+
mcp_names: Optional dictionary mapping operationId to desired component names.
|
|
722
|
+
If an operationId is not in the dictionary, falls back to using the
|
|
723
|
+
operationId up to the first double underscore. If no operationId exists,
|
|
724
|
+
falls back to slugified summary or path-based naming.
|
|
725
|
+
All names are truncated to 56 characters maximum.
|
|
595
726
|
timeout: Optional timeout (in seconds) for all requests
|
|
596
727
|
**settings: Additional settings for FastMCP
|
|
597
728
|
"""
|
|
@@ -599,6 +730,18 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
599
730
|
|
|
600
731
|
self._client = client
|
|
601
732
|
self._timeout = timeout
|
|
733
|
+
self._route_map_fn = route_map_fn
|
|
734
|
+
self._mcp_component_fn = mcp_component_fn
|
|
735
|
+
self._mcp_names = mcp_names or {}
|
|
736
|
+
|
|
737
|
+
# Keep track of names to detect collisions
|
|
738
|
+
self._used_names = {
|
|
739
|
+
"tool": Counter(),
|
|
740
|
+
"resource": Counter(),
|
|
741
|
+
"resource_template": Counter(),
|
|
742
|
+
"prompt": Counter(),
|
|
743
|
+
}
|
|
744
|
+
|
|
602
745
|
http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
|
|
603
746
|
|
|
604
747
|
# Process routes
|
|
@@ -607,34 +750,97 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
607
750
|
# Determine route type based on mappings or default rules
|
|
608
751
|
route_type = _determine_route_type(route, route_maps)
|
|
609
752
|
|
|
610
|
-
#
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
753
|
+
# Call route_map_fn if provided
|
|
754
|
+
if self._route_map_fn is not None:
|
|
755
|
+
try:
|
|
756
|
+
result = self._route_map_fn(route, route_type)
|
|
757
|
+
if result is not None:
|
|
758
|
+
route_type = result
|
|
759
|
+
logger.debug(
|
|
760
|
+
f"Route {route.method} {route.path} mapping customized by route_map_fn: "
|
|
761
|
+
f"type={route_type.name}"
|
|
762
|
+
)
|
|
763
|
+
except Exception as e:
|
|
764
|
+
logger.warning(
|
|
765
|
+
f"Error in route_map_fn for {route.method} {route.path}: {e}. "
|
|
766
|
+
f"Using default values."
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Generate a default name from the route
|
|
770
|
+
component_name = self._generate_default_name(route, route_type)
|
|
771
|
+
|
|
772
|
+
if route_type == MCPType.TOOL:
|
|
773
|
+
self._create_openapi_tool(route, component_name)
|
|
774
|
+
elif route_type == MCPType.RESOURCE:
|
|
775
|
+
self._create_openapi_resource(route, component_name)
|
|
776
|
+
elif route_type == MCPType.RESOURCE_TEMPLATE:
|
|
777
|
+
self._create_openapi_template(route, component_name)
|
|
778
|
+
elif route_type == MCPType.EXCLUDE:
|
|
779
|
+
logger.info(f"Excluding route: {route.method} {route.path}")
|
|
631
780
|
|
|
632
781
|
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
|
|
633
782
|
|
|
634
|
-
def
|
|
783
|
+
def _generate_default_name(
|
|
784
|
+
self, route: openapi.HTTPRoute, mcp_type: MCPType
|
|
785
|
+
) -> str:
|
|
786
|
+
"""Generate a default name from the route using the configured strategy."""
|
|
787
|
+
name = ""
|
|
788
|
+
|
|
789
|
+
# First check if there's a custom mapping for this operationId
|
|
790
|
+
if route.operation_id:
|
|
791
|
+
if route.operation_id in self._mcp_names:
|
|
792
|
+
name = self._mcp_names[route.operation_id]
|
|
793
|
+
else:
|
|
794
|
+
# If there's a double underscore in the operationId, use the first part
|
|
795
|
+
name = route.operation_id.split("__")[0]
|
|
796
|
+
else:
|
|
797
|
+
name = route.summary or f"{route.method}_{route.path}"
|
|
798
|
+
|
|
799
|
+
name = _slugify(name)
|
|
800
|
+
|
|
801
|
+
# Truncate to 56 characters maximum
|
|
802
|
+
if len(name) > 56:
|
|
803
|
+
name = name[:56]
|
|
804
|
+
|
|
805
|
+
return name
|
|
806
|
+
|
|
807
|
+
def _get_unique_name(
|
|
808
|
+
self,
|
|
809
|
+
name: str,
|
|
810
|
+
component_type: Literal["tool", "resource", "resource_template", "prompt"],
|
|
811
|
+
) -> str:
|
|
812
|
+
"""
|
|
813
|
+
Ensure the name is unique within its component type by appending numbers if needed.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
name: The proposed name
|
|
817
|
+
component_type: The type of component ("tools", "resources", or "templates")
|
|
818
|
+
|
|
819
|
+
Returns:
|
|
820
|
+
str: A unique name for the component
|
|
821
|
+
"""
|
|
822
|
+
# Check if the name is already used
|
|
823
|
+
self._used_names[component_type][name] += 1
|
|
824
|
+
if self._used_names[component_type][name] == 1:
|
|
825
|
+
return name
|
|
826
|
+
|
|
827
|
+
else:
|
|
828
|
+
# Create the new name
|
|
829
|
+
new_name = f"{name}_{self._used_names[component_type][name]}"
|
|
830
|
+
logger.debug(
|
|
831
|
+
f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
|
|
832
|
+
f"Using '{new_name}' instead."
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
return new_name
|
|
836
|
+
|
|
837
|
+
def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
|
|
635
838
|
"""Creates and registers an OpenAPITool with enhanced description."""
|
|
636
839
|
combined_schema = _combine_schemas(route)
|
|
637
|
-
|
|
840
|
+
|
|
841
|
+
# Get a unique tool name
|
|
842
|
+
tool_name = self._get_unique_name(name, "tool")
|
|
843
|
+
|
|
638
844
|
base_description = (
|
|
639
845
|
route.description
|
|
640
846
|
or route.summary
|
|
@@ -658,16 +864,30 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
658
864
|
tags=set(route.tags or []),
|
|
659
865
|
timeout=self._timeout,
|
|
660
866
|
)
|
|
867
|
+
|
|
868
|
+
# Call component_fn if provided
|
|
869
|
+
if self._mcp_component_fn is not None:
|
|
870
|
+
try:
|
|
871
|
+
self._mcp_component_fn(route, tool)
|
|
872
|
+
logger.debug(f"Tool {tool_name} customized by component_fn")
|
|
873
|
+
except Exception as e:
|
|
874
|
+
logger.warning(
|
|
875
|
+
f"Error in component_fn for tool {tool_name}: {e}. "
|
|
876
|
+
f"Using component as-is."
|
|
877
|
+
)
|
|
878
|
+
|
|
661
879
|
# Register the tool by directly assigning to the tools dictionary
|
|
662
880
|
self._tool_manager._tools[tool_name] = tool
|
|
663
881
|
logger.debug(
|
|
664
882
|
f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
|
|
665
883
|
)
|
|
666
884
|
|
|
667
|
-
def _create_openapi_resource(self, route: openapi.HTTPRoute,
|
|
885
|
+
def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
|
|
668
886
|
"""Creates and registers an OpenAPIResource with enhanced description."""
|
|
669
|
-
|
|
670
|
-
|
|
887
|
+
# Get a unique resource name
|
|
888
|
+
resource_name = self._get_unique_name(name, "resource")
|
|
889
|
+
|
|
890
|
+
resource_uri = f"resource://{resource_name}"
|
|
671
891
|
base_description = (
|
|
672
892
|
route.description or route.summary or f"Represents {route.path}"
|
|
673
893
|
)
|
|
@@ -689,19 +909,33 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
689
909
|
tags=set(route.tags or []),
|
|
690
910
|
timeout=self._timeout,
|
|
691
911
|
)
|
|
912
|
+
|
|
913
|
+
# Call component_fn if provided
|
|
914
|
+
if self._mcp_component_fn is not None:
|
|
915
|
+
try:
|
|
916
|
+
self._mcp_component_fn(route, resource)
|
|
917
|
+
logger.debug(f"Resource {resource_uri} customized by component_fn")
|
|
918
|
+
except Exception as e:
|
|
919
|
+
logger.warning(
|
|
920
|
+
f"Error in component_fn for resource {resource_uri}: {e}. "
|
|
921
|
+
f"Using component as-is."
|
|
922
|
+
)
|
|
923
|
+
|
|
692
924
|
# Register the resource by directly assigning to the resources dictionary
|
|
693
925
|
self._resource_manager._resources[str(resource.uri)] = resource
|
|
694
926
|
logger.debug(
|
|
695
927
|
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
|
|
696
928
|
)
|
|
697
929
|
|
|
698
|
-
def _create_openapi_template(self, route: openapi.HTTPRoute,
|
|
930
|
+
def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
|
|
699
931
|
"""Creates and registers an OpenAPIResourceTemplate with enhanced description."""
|
|
700
|
-
|
|
932
|
+
# Get a unique template name
|
|
933
|
+
template_name = self._get_unique_name(name, "resource_template")
|
|
934
|
+
|
|
701
935
|
path_params = [p.name for p in route.parameters if p.location == "path"]
|
|
702
936
|
path_params.sort() # Sort for consistent URIs
|
|
703
937
|
|
|
704
|
-
uri_template_str = f"resource://
|
|
938
|
+
uri_template_str = f"resource://{template_name}"
|
|
705
939
|
if path_params:
|
|
706
940
|
uri_template_str += "/" + "/".join(f"{{{p}}}" for p in path_params)
|
|
707
941
|
|
|
@@ -749,13 +983,20 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
749
983
|
tags=set(route.tags or []),
|
|
750
984
|
timeout=self._timeout,
|
|
751
985
|
)
|
|
986
|
+
|
|
987
|
+
# Call component_fn if provided
|
|
988
|
+
if self._mcp_component_fn is not None:
|
|
989
|
+
try:
|
|
990
|
+
self._mcp_component_fn(route, template)
|
|
991
|
+
logger.debug(f"Template {uri_template_str} customized by component_fn")
|
|
992
|
+
except Exception as e:
|
|
993
|
+
logger.warning(
|
|
994
|
+
f"Error in component_fn for template {uri_template_str}: {e}. "
|
|
995
|
+
f"Using component as-is."
|
|
996
|
+
)
|
|
997
|
+
|
|
752
998
|
# Register the template by directly assigning to the templates dictionary
|
|
753
999
|
self._resource_manager._templates[uri_template_str] = template
|
|
754
1000
|
logger.debug(
|
|
755
1001
|
f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path}) with tags: {route.tags}"
|
|
756
1002
|
)
|
|
757
|
-
|
|
758
|
-
async def _mcp_call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
759
|
-
"""Override the call_tool method to return the raw result without converting to content."""
|
|
760
|
-
result = await self._tool_manager.call_tool(name, arguments)
|
|
761
|
-
return result
|