fastmcp 2.4.0__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/client/client.py +23 -1
- fastmcp/client/transports.py +68 -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/http.py +2 -0
- fastmcp/server/openapi.py +324 -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.0.dist-info}/METADATA +4 -4
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.dist-info}/RECORD +20 -20
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.4.0.dist-info → fastmcp-2.5.0.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_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
|
-
"""
|
|
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] | Literal["*"]
|
|
51
|
-
pattern: Pattern[str] | str
|
|
52
|
-
|
|
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".*\{.*\}.*",
|
|
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".*",
|
|
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
|
-
) ->
|
|
193
|
+
) -> MCPType:
|
|
76
194
|
"""
|
|
77
195
|
Determines the FastMCP component type based on the route and mappings.
|
|
78
196
|
|
|
@@ -81,7 +199,7 @@ def _determine_route_type(
|
|
|
81
199
|
mappings: List of RouteMap objects in priority order
|
|
82
200
|
|
|
83
201
|
Returns:
|
|
84
|
-
|
|
202
|
+
MCPType for this route
|
|
85
203
|
"""
|
|
86
204
|
# Check mappings in priority order (first match wins)
|
|
87
205
|
for route_map in mappings:
|
|
@@ -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.
|
|
227
|
+
f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
|
|
99
228
|
)
|
|
100
|
-
return route_map.
|
|
229
|
+
return route_map.mcp_type
|
|
101
230
|
|
|
102
231
|
# 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
|
|
232
|
+
return MCPType.TOOL
|
|
111
233
|
|
|
112
234
|
|
|
113
235
|
class OpenAPITool(Tool):
|
|
@@ -288,13 +410,21 @@ class OpenAPITool(Tool):
|
|
|
288
410
|
|
|
289
411
|
# Prepare headers - fix typing by ensuring all values are strings
|
|
290
412
|
headers = {}
|
|
413
|
+
|
|
414
|
+
# Start with OpenAPI-defined header parameters
|
|
415
|
+
openapi_headers = {}
|
|
291
416
|
for p in self._route.parameters:
|
|
292
417
|
if (
|
|
293
418
|
p.location == "header"
|
|
294
419
|
and p.name in kwargs
|
|
295
420
|
and kwargs[p.name] is not None
|
|
296
421
|
):
|
|
297
|
-
|
|
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)
|
|
298
428
|
|
|
299
429
|
# Prepare request body
|
|
300
430
|
json_data = None
|
|
@@ -442,10 +572,16 @@ class OpenAPIResource(Resource):
|
|
|
442
572
|
if value is not None and value != "":
|
|
443
573
|
query_params[param.name] = value
|
|
444
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
|
+
|
|
445
580
|
response = await self._client.request(
|
|
446
581
|
method=self._route.method,
|
|
447
582
|
url=path,
|
|
448
583
|
params=query_params,
|
|
584
|
+
headers=headers,
|
|
449
585
|
timeout=self._timeout,
|
|
450
586
|
)
|
|
451
587
|
|
|
@@ -546,7 +682,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
546
682
|
|
|
547
683
|
Example:
|
|
548
684
|
```python
|
|
549
|
-
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap,
|
|
685
|
+
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
|
|
550
686
|
import httpx
|
|
551
687
|
|
|
552
688
|
# Define custom route mappings
|
|
@@ -555,17 +691,17 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
555
691
|
RouteMap(
|
|
556
692
|
methods=["GET", "POST", "PATCH"],
|
|
557
693
|
pattern=r".*/users/.*",
|
|
558
|
-
|
|
694
|
+
mcp_type=MCPType.RESOURCE_TEMPLATE
|
|
559
695
|
),
|
|
560
696
|
# Map all analytics endpoints to Tool
|
|
561
697
|
RouteMap(
|
|
562
698
|
methods=["GET"],
|
|
563
699
|
pattern=r".*/analytics/.*",
|
|
564
|
-
|
|
700
|
+
mcp_type=MCPType.TOOL
|
|
565
701
|
),
|
|
566
702
|
]
|
|
567
703
|
|
|
568
|
-
# Create server with custom mappings
|
|
704
|
+
# Create server with custom mappings and route mapper
|
|
569
705
|
server = FastMCPOpenAPI(
|
|
570
706
|
openapi_spec=spec,
|
|
571
707
|
client=httpx.AsyncClient(),
|
|
@@ -581,6 +717,9 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
581
717
|
client: httpx.AsyncClient,
|
|
582
718
|
name: str | None = None,
|
|
583
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,
|
|
584
723
|
timeout: float | None = None,
|
|
585
724
|
**settings: Any,
|
|
586
725
|
):
|
|
@@ -592,6 +731,17 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
592
731
|
client: httpx AsyncClient for making HTTP requests
|
|
593
732
|
name: Optional name for the server
|
|
594
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.
|
|
595
745
|
timeout: Optional timeout (in seconds) for all requests
|
|
596
746
|
**settings: Additional settings for FastMCP
|
|
597
747
|
"""
|
|
@@ -599,6 +749,18 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
599
749
|
|
|
600
750
|
self._client = client
|
|
601
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
|
+
|
|
602
764
|
http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
|
|
603
765
|
|
|
604
766
|
# Process routes
|
|
@@ -607,34 +769,97 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
607
769
|
# Determine route type based on mappings or default rules
|
|
608
770
|
route_type = _determine_route_type(route, route_maps)
|
|
609
771
|
|
|
610
|
-
#
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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}")
|
|
631
799
|
|
|
632
800
|
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
|
|
633
801
|
|
|
634
|
-
def
|
|
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):
|
|
635
857
|
"""Creates and registers an OpenAPITool with enhanced description."""
|
|
636
858
|
combined_schema = _combine_schemas(route)
|
|
637
|
-
|
|
859
|
+
|
|
860
|
+
# Get a unique tool name
|
|
861
|
+
tool_name = self._get_unique_name(name, "tool")
|
|
862
|
+
|
|
638
863
|
base_description = (
|
|
639
864
|
route.description
|
|
640
865
|
or route.summary
|
|
@@ -658,16 +883,30 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
658
883
|
tags=set(route.tags or []),
|
|
659
884
|
timeout=self._timeout,
|
|
660
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
|
+
|
|
661
898
|
# Register the tool by directly assigning to the tools dictionary
|
|
662
899
|
self._tool_manager._tools[tool_name] = tool
|
|
663
900
|
logger.debug(
|
|
664
901
|
f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
|
|
665
902
|
)
|
|
666
903
|
|
|
667
|
-
def _create_openapi_resource(self, route: openapi.HTTPRoute,
|
|
904
|
+
def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
|
|
668
905
|
"""Creates and registers an OpenAPIResource with enhanced description."""
|
|
669
|
-
|
|
670
|
-
|
|
906
|
+
# Get a unique resource name
|
|
907
|
+
resource_name = self._get_unique_name(name, "resource")
|
|
908
|
+
|
|
909
|
+
resource_uri = f"resource://{resource_name}"
|
|
671
910
|
base_description = (
|
|
672
911
|
route.description or route.summary or f"Represents {route.path}"
|
|
673
912
|
)
|
|
@@ -689,19 +928,33 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
689
928
|
tags=set(route.tags or []),
|
|
690
929
|
timeout=self._timeout,
|
|
691
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
|
+
|
|
692
943
|
# Register the resource by directly assigning to the resources dictionary
|
|
693
944
|
self._resource_manager._resources[str(resource.uri)] = resource
|
|
694
945
|
logger.debug(
|
|
695
946
|
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
|
|
696
947
|
)
|
|
697
948
|
|
|
698
|
-
def _create_openapi_template(self, route: openapi.HTTPRoute,
|
|
949
|
+
def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
|
|
699
950
|
"""Creates and registers an OpenAPIResourceTemplate with enhanced description."""
|
|
700
|
-
|
|
951
|
+
# Get a unique template name
|
|
952
|
+
template_name = self._get_unique_name(name, "resource_template")
|
|
953
|
+
|
|
701
954
|
path_params = [p.name for p in route.parameters if p.location == "path"]
|
|
702
955
|
path_params.sort() # Sort for consistent URIs
|
|
703
956
|
|
|
704
|
-
uri_template_str = f"resource://
|
|
957
|
+
uri_template_str = f"resource://{template_name}"
|
|
705
958
|
if path_params:
|
|
706
959
|
uri_template_str += "/" + "/".join(f"{{{p}}}" for p in path_params)
|
|
707
960
|
|
|
@@ -749,13 +1002,20 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
749
1002
|
tags=set(route.tags or []),
|
|
750
1003
|
timeout=self._timeout,
|
|
751
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
|
+
|
|
752
1017
|
# Register the template by directly assigning to the templates dictionary
|
|
753
1018
|
self._resource_manager._templates[uri_template_str] = template
|
|
754
1019
|
logger.debug(
|
|
755
1020
|
f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path}) with tags: {route.tags}"
|
|
756
1021
|
)
|
|
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
|