fastmcp 2.14.5__py3-none-any.whl → 3.0.0b1__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/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.5.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.5.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""OpenAPIProvider for creating MCP components from OpenAPI specifications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from jsonschema_path import SchemaPath
|
|
11
|
+
|
|
12
|
+
from fastmcp.prompts import Prompt
|
|
13
|
+
from fastmcp.resources import Resource, ResourceTemplate
|
|
14
|
+
from fastmcp.server.providers.base import Provider
|
|
15
|
+
from fastmcp.server.providers.openapi.components import (
|
|
16
|
+
OpenAPIResource,
|
|
17
|
+
OpenAPIResourceTemplate,
|
|
18
|
+
OpenAPITool,
|
|
19
|
+
_slugify,
|
|
20
|
+
)
|
|
21
|
+
from fastmcp.server.providers.openapi.routing import (
|
|
22
|
+
DEFAULT_ROUTE_MAPPINGS,
|
|
23
|
+
ComponentFn,
|
|
24
|
+
MCPType,
|
|
25
|
+
RouteMap,
|
|
26
|
+
RouteMapFn,
|
|
27
|
+
_determine_route_type,
|
|
28
|
+
)
|
|
29
|
+
from fastmcp.tools.tool import Tool
|
|
30
|
+
from fastmcp.utilities.components import FastMCPComponent
|
|
31
|
+
from fastmcp.utilities.logging import get_logger
|
|
32
|
+
from fastmcp.utilities.openapi import (
|
|
33
|
+
HTTPRoute,
|
|
34
|
+
extract_output_schema_from_responses,
|
|
35
|
+
format_simple_description,
|
|
36
|
+
parse_openapi_to_http_routes,
|
|
37
|
+
)
|
|
38
|
+
from fastmcp.utilities.openapi.director import RequestDirector
|
|
39
|
+
from fastmcp.utilities.versions import VersionSpec, version_sort_key
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"OpenAPIProvider",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
logger = get_logger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class OpenAPIProvider(Provider):
|
|
49
|
+
"""Provider that creates MCP components from an OpenAPI specification.
|
|
50
|
+
|
|
51
|
+
Components are created eagerly during initialization by parsing the OpenAPI
|
|
52
|
+
spec. Each component makes HTTP calls to the described API endpoints.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
```python
|
|
56
|
+
from fastmcp import FastMCP
|
|
57
|
+
from fastmcp.server.providers.openapi import OpenAPIProvider
|
|
58
|
+
import httpx
|
|
59
|
+
|
|
60
|
+
client = httpx.AsyncClient(base_url="https://api.example.com")
|
|
61
|
+
provider = OpenAPIProvider(openapi_spec=spec, client=client)
|
|
62
|
+
|
|
63
|
+
mcp = FastMCP("API Server")
|
|
64
|
+
mcp.add_provider(provider)
|
|
65
|
+
```
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
openapi_spec: dict[str, Any],
|
|
71
|
+
client: httpx.AsyncClient,
|
|
72
|
+
*,
|
|
73
|
+
route_maps: list[RouteMap] | None = None,
|
|
74
|
+
route_map_fn: RouteMapFn | None = None,
|
|
75
|
+
mcp_component_fn: ComponentFn | None = None,
|
|
76
|
+
mcp_names: dict[str, str] | None = None,
|
|
77
|
+
tags: set[str] | None = None,
|
|
78
|
+
timeout: float | None = None,
|
|
79
|
+
):
|
|
80
|
+
"""Initialize provider by parsing OpenAPI spec and creating components.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
openapi_spec: OpenAPI schema as a dictionary
|
|
84
|
+
client: httpx AsyncClient for making HTTP requests
|
|
85
|
+
route_maps: Optional list of RouteMap objects defining route mappings
|
|
86
|
+
route_map_fn: Optional callable for advanced route type mapping
|
|
87
|
+
mcp_component_fn: Optional callable for component customization
|
|
88
|
+
mcp_names: Optional dictionary mapping operationId to component names
|
|
89
|
+
tags: Optional set of tags to add to all components
|
|
90
|
+
timeout: Optional timeout (in seconds) for all requests
|
|
91
|
+
"""
|
|
92
|
+
super().__init__()
|
|
93
|
+
|
|
94
|
+
self._client = client
|
|
95
|
+
self._timeout = timeout
|
|
96
|
+
self._mcp_component_fn = mcp_component_fn
|
|
97
|
+
|
|
98
|
+
# Keep track of names to detect collisions
|
|
99
|
+
self._used_names: dict[str, Counter[str]] = {
|
|
100
|
+
"tool": Counter(),
|
|
101
|
+
"resource": Counter(),
|
|
102
|
+
"resource_template": Counter(),
|
|
103
|
+
"prompt": Counter(),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Pre-created component storage
|
|
107
|
+
self._tools: dict[str, OpenAPITool] = {}
|
|
108
|
+
self._resources: dict[str, OpenAPIResource] = {}
|
|
109
|
+
self._templates: dict[str, OpenAPIResourceTemplate] = {}
|
|
110
|
+
|
|
111
|
+
# Create openapi-core Spec and RequestDirector
|
|
112
|
+
try:
|
|
113
|
+
self._spec = SchemaPath.from_dict(openapi_spec) # type: ignore[arg-type]
|
|
114
|
+
self._director = RequestDirector(self._spec)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.exception("Failed to initialize RequestDirector")
|
|
117
|
+
raise ValueError(f"Invalid OpenAPI specification: {e}") from e
|
|
118
|
+
|
|
119
|
+
http_routes = parse_openapi_to_http_routes(openapi_spec)
|
|
120
|
+
|
|
121
|
+
# Process routes
|
|
122
|
+
route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS
|
|
123
|
+
for route in http_routes:
|
|
124
|
+
route_map = _determine_route_type(route, route_maps)
|
|
125
|
+
route_type = route_map.mcp_type
|
|
126
|
+
|
|
127
|
+
if route_map_fn is not None:
|
|
128
|
+
try:
|
|
129
|
+
result = route_map_fn(route, route_type)
|
|
130
|
+
if result is not None:
|
|
131
|
+
route_type = result
|
|
132
|
+
logger.debug(
|
|
133
|
+
f"Route {route.method} {route.path} mapping customized: "
|
|
134
|
+
f"type={route_type.name}"
|
|
135
|
+
)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"Error in route_map_fn for {route.method} {route.path}: {e}. "
|
|
139
|
+
f"Using default values."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
component_name = self._generate_default_name(route, mcp_names)
|
|
143
|
+
route_tags = set(route.tags) | route_map.mcp_tags | (tags or set())
|
|
144
|
+
|
|
145
|
+
if route_type == MCPType.TOOL:
|
|
146
|
+
self._create_openapi_tool(route, component_name, tags=route_tags)
|
|
147
|
+
elif route_type == MCPType.RESOURCE:
|
|
148
|
+
self._create_openapi_resource(route, component_name, tags=route_tags)
|
|
149
|
+
elif route_type == MCPType.RESOURCE_TEMPLATE:
|
|
150
|
+
self._create_openapi_template(route, component_name, tags=route_tags)
|
|
151
|
+
elif route_type == MCPType.EXCLUDE:
|
|
152
|
+
logger.debug(f"Excluding route: {route.method} {route.path}")
|
|
153
|
+
|
|
154
|
+
logger.debug(f"Created OpenAPIProvider with {len(http_routes)} routes")
|
|
155
|
+
|
|
156
|
+
def _generate_default_name(
|
|
157
|
+
self, route: HTTPRoute, mcp_names_map: dict[str, str] | None = None
|
|
158
|
+
) -> str:
|
|
159
|
+
"""Generate a default name from the route."""
|
|
160
|
+
mcp_names_map = mcp_names_map or {}
|
|
161
|
+
|
|
162
|
+
if route.operation_id:
|
|
163
|
+
if route.operation_id in mcp_names_map:
|
|
164
|
+
name = mcp_names_map[route.operation_id]
|
|
165
|
+
else:
|
|
166
|
+
name = route.operation_id.split("__")[0]
|
|
167
|
+
else:
|
|
168
|
+
name = route.summary or f"{route.method}_{route.path}"
|
|
169
|
+
|
|
170
|
+
name = _slugify(name)
|
|
171
|
+
|
|
172
|
+
if len(name) > 56:
|
|
173
|
+
name = name[:56]
|
|
174
|
+
|
|
175
|
+
return name
|
|
176
|
+
|
|
177
|
+
def _get_unique_name(
|
|
178
|
+
self,
|
|
179
|
+
name: str,
|
|
180
|
+
component_type: Literal["tool", "resource", "resource_template", "prompt"],
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Ensure the name is unique by appending numbers if needed."""
|
|
183
|
+
self._used_names[component_type][name] += 1
|
|
184
|
+
if self._used_names[component_type][name] == 1:
|
|
185
|
+
return name
|
|
186
|
+
|
|
187
|
+
new_name = f"{name}_{self._used_names[component_type][name]}"
|
|
188
|
+
logger.debug(
|
|
189
|
+
f"Name collision: '{name}' exists as {component_type}. Using '{new_name}'."
|
|
190
|
+
)
|
|
191
|
+
return new_name
|
|
192
|
+
|
|
193
|
+
def _create_openapi_tool(
|
|
194
|
+
self,
|
|
195
|
+
route: HTTPRoute,
|
|
196
|
+
name: str,
|
|
197
|
+
tags: set[str],
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Create and register an OpenAPITool."""
|
|
200
|
+
combined_schema = route.flat_param_schema
|
|
201
|
+
output_schema = extract_output_schema_from_responses(
|
|
202
|
+
route.responses,
|
|
203
|
+
route.response_schemas,
|
|
204
|
+
route.openapi_version,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
tool_name = self._get_unique_name(name, "tool")
|
|
208
|
+
base_description = (
|
|
209
|
+
route.description
|
|
210
|
+
or route.summary
|
|
211
|
+
or f"Executes {route.method} {route.path}"
|
|
212
|
+
)
|
|
213
|
+
enhanced_description = format_simple_description(
|
|
214
|
+
base_description=base_description,
|
|
215
|
+
parameters=route.parameters,
|
|
216
|
+
request_body=route.request_body,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
tool = OpenAPITool(
|
|
220
|
+
client=self._client,
|
|
221
|
+
route=route,
|
|
222
|
+
director=self._director,
|
|
223
|
+
name=tool_name,
|
|
224
|
+
description=enhanced_description,
|
|
225
|
+
parameters=combined_schema,
|
|
226
|
+
output_schema=output_schema,
|
|
227
|
+
tags=set(route.tags or []) | tags,
|
|
228
|
+
timeout=self._timeout,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if self._mcp_component_fn is not None:
|
|
232
|
+
try:
|
|
233
|
+
self._mcp_component_fn(route, tool)
|
|
234
|
+
logger.debug(f"Tool {tool_name} customized by component_fn")
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.warning(f"Error in component_fn for tool {tool_name}: {e}")
|
|
237
|
+
|
|
238
|
+
self._tools[tool.name] = tool
|
|
239
|
+
|
|
240
|
+
def _create_openapi_resource(
|
|
241
|
+
self,
|
|
242
|
+
route: HTTPRoute,
|
|
243
|
+
name: str,
|
|
244
|
+
tags: set[str],
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Create and register an OpenAPIResource."""
|
|
247
|
+
resource_name = self._get_unique_name(name, "resource")
|
|
248
|
+
resource_uri = f"resource://{resource_name}"
|
|
249
|
+
base_description = (
|
|
250
|
+
route.description or route.summary or f"Represents {route.path}"
|
|
251
|
+
)
|
|
252
|
+
enhanced_description = format_simple_description(
|
|
253
|
+
base_description=base_description,
|
|
254
|
+
parameters=route.parameters,
|
|
255
|
+
request_body=route.request_body,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
resource = OpenAPIResource(
|
|
259
|
+
client=self._client,
|
|
260
|
+
route=route,
|
|
261
|
+
director=self._director,
|
|
262
|
+
uri=resource_uri,
|
|
263
|
+
name=resource_name,
|
|
264
|
+
description=enhanced_description,
|
|
265
|
+
tags=set(route.tags or []) | tags,
|
|
266
|
+
timeout=self._timeout,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if self._mcp_component_fn is not None:
|
|
270
|
+
try:
|
|
271
|
+
self._mcp_component_fn(route, resource)
|
|
272
|
+
logger.debug(f"Resource {resource_uri} customized by component_fn")
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.warning(
|
|
275
|
+
f"Error in component_fn for resource {resource_uri}: {e}"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
self._resources[str(resource.uri)] = resource
|
|
279
|
+
|
|
280
|
+
def _create_openapi_template(
|
|
281
|
+
self,
|
|
282
|
+
route: HTTPRoute,
|
|
283
|
+
name: str,
|
|
284
|
+
tags: set[str],
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Create and register an OpenAPIResourceTemplate."""
|
|
287
|
+
template_name = self._get_unique_name(name, "resource_template")
|
|
288
|
+
|
|
289
|
+
path_params = sorted(p.name for p in route.parameters if p.location == "path")
|
|
290
|
+
uri_template_str = f"resource://{template_name}"
|
|
291
|
+
if path_params:
|
|
292
|
+
uri_template_str += "/" + "/".join(f"{{{p}}}" for p in path_params)
|
|
293
|
+
|
|
294
|
+
base_description = (
|
|
295
|
+
route.description or route.summary or f"Template for {route.path}"
|
|
296
|
+
)
|
|
297
|
+
enhanced_description = format_simple_description(
|
|
298
|
+
base_description=base_description,
|
|
299
|
+
parameters=route.parameters,
|
|
300
|
+
request_body=route.request_body,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
template_params_schema = {
|
|
304
|
+
"type": "object",
|
|
305
|
+
"properties": {
|
|
306
|
+
p.name: {
|
|
307
|
+
**(p.schema_.copy() if isinstance(p.schema_, dict) else {}),
|
|
308
|
+
**(
|
|
309
|
+
{"description": p.description}
|
|
310
|
+
if p.description
|
|
311
|
+
and not (
|
|
312
|
+
isinstance(p.schema_, dict) and "description" in p.schema_
|
|
313
|
+
)
|
|
314
|
+
else {}
|
|
315
|
+
),
|
|
316
|
+
}
|
|
317
|
+
for p in route.parameters
|
|
318
|
+
if p.location == "path"
|
|
319
|
+
},
|
|
320
|
+
"required": [
|
|
321
|
+
p.name for p in route.parameters if p.location == "path" and p.required
|
|
322
|
+
],
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
template = OpenAPIResourceTemplate(
|
|
326
|
+
client=self._client,
|
|
327
|
+
route=route,
|
|
328
|
+
director=self._director,
|
|
329
|
+
uri_template=uri_template_str,
|
|
330
|
+
name=template_name,
|
|
331
|
+
description=enhanced_description,
|
|
332
|
+
parameters=template_params_schema,
|
|
333
|
+
tags=set(route.tags or []) | tags,
|
|
334
|
+
timeout=self._timeout,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if self._mcp_component_fn is not None:
|
|
338
|
+
try:
|
|
339
|
+
self._mcp_component_fn(route, template)
|
|
340
|
+
logger.debug(f"Template {uri_template_str} customized by component_fn")
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.warning(
|
|
343
|
+
f"Error in component_fn for template {uri_template_str}: {e}"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
self._templates[template.uri_template] = template
|
|
347
|
+
|
|
348
|
+
# -------------------------------------------------------------------------
|
|
349
|
+
# Provider interface
|
|
350
|
+
# -------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
async def _list_tools(self) -> Sequence[Tool]:
|
|
353
|
+
"""Return all tools created from the OpenAPI spec."""
|
|
354
|
+
return list(self._tools.values())
|
|
355
|
+
|
|
356
|
+
async def _get_tool(
|
|
357
|
+
self, name: str, version: VersionSpec | None = None
|
|
358
|
+
) -> Tool | None:
|
|
359
|
+
"""Get a tool by name."""
|
|
360
|
+
tool = self._tools.get(name)
|
|
361
|
+
if tool is None:
|
|
362
|
+
return None
|
|
363
|
+
if version is not None and not version.matches(tool.version):
|
|
364
|
+
return None
|
|
365
|
+
return tool
|
|
366
|
+
|
|
367
|
+
async def _list_resources(self) -> Sequence[Resource]:
|
|
368
|
+
"""Return all resources created from the OpenAPI spec."""
|
|
369
|
+
return list(self._resources.values())
|
|
370
|
+
|
|
371
|
+
async def _get_resource(
|
|
372
|
+
self, uri: str, version: VersionSpec | None = None
|
|
373
|
+
) -> Resource | None:
|
|
374
|
+
"""Get a resource by URI."""
|
|
375
|
+
resource = self._resources.get(uri)
|
|
376
|
+
if resource is None:
|
|
377
|
+
return None
|
|
378
|
+
if version is not None and not version.matches(resource.version):
|
|
379
|
+
return None
|
|
380
|
+
return resource
|
|
381
|
+
|
|
382
|
+
async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:
|
|
383
|
+
"""Return all resource templates created from the OpenAPI spec."""
|
|
384
|
+
return list(self._templates.values())
|
|
385
|
+
|
|
386
|
+
async def _get_resource_template(
|
|
387
|
+
self, uri: str, version: VersionSpec | None = None
|
|
388
|
+
) -> ResourceTemplate | None:
|
|
389
|
+
"""Get a resource template that matches the given URI."""
|
|
390
|
+
matching = [t for t in self._templates.values() if t.matches(uri) is not None]
|
|
391
|
+
if not matching:
|
|
392
|
+
return None
|
|
393
|
+
if version is not None:
|
|
394
|
+
matching = [t for t in matching if version.matches(t.version)]
|
|
395
|
+
if not matching:
|
|
396
|
+
return None
|
|
397
|
+
return max(matching, key=version_sort_key) # type: ignore[type-var]
|
|
398
|
+
|
|
399
|
+
async def _list_prompts(self) -> Sequence[Prompt]:
|
|
400
|
+
"""Return empty list - OpenAPI doesn't create prompts."""
|
|
401
|
+
return []
|
|
402
|
+
|
|
403
|
+
async def get_tasks(self) -> Sequence[FastMCPComponent]:
|
|
404
|
+
"""Return empty list - OpenAPI components don't support tasks."""
|
|
405
|
+
return []
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Route mapping logic for OpenAPI operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from re import Pattern
|
|
10
|
+
from typing import TYPE_CHECKING, Literal
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from fastmcp.server.providers.openapi.components import (
|
|
14
|
+
OpenAPIResource,
|
|
15
|
+
OpenAPIResourceTemplate,
|
|
16
|
+
OpenAPITool,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from fastmcp.utilities.logging import get_logger
|
|
20
|
+
from fastmcp.utilities.openapi import HttpMethod, HTTPRoute
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ComponentFn",
|
|
24
|
+
"MCPType",
|
|
25
|
+
"RouteMap",
|
|
26
|
+
"RouteMapFn",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
# Type definitions for the mapping functions
|
|
32
|
+
RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
|
|
33
|
+
ComponentFn = Callable[
|
|
34
|
+
[
|
|
35
|
+
HTTPRoute,
|
|
36
|
+
"OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate",
|
|
37
|
+
],
|
|
38
|
+
None,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MCPType(enum.Enum):
|
|
43
|
+
"""Type of FastMCP component to create from a route.
|
|
44
|
+
|
|
45
|
+
Enum values:
|
|
46
|
+
TOOL: Convert the route to a callable Tool
|
|
47
|
+
RESOURCE: Convert the route to a Resource (typically GET endpoints)
|
|
48
|
+
RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)
|
|
49
|
+
EXCLUDE: Exclude the route from being converted to any MCP component
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
TOOL = "TOOL"
|
|
53
|
+
RESOURCE = "RESOURCE"
|
|
54
|
+
RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
|
|
55
|
+
EXCLUDE = "EXCLUDE"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(kw_only=True)
|
|
59
|
+
class RouteMap:
|
|
60
|
+
"""Mapping configuration for HTTP routes to FastMCP component types."""
|
|
61
|
+
|
|
62
|
+
methods: list[HttpMethod] | Literal["*"] = field(default="*")
|
|
63
|
+
pattern: Pattern[str] | str = field(default=r".*")
|
|
64
|
+
|
|
65
|
+
tags: set[str] = field(
|
|
66
|
+
default_factory=set,
|
|
67
|
+
metadata={"description": "A set of tags to match. All tags must match."},
|
|
68
|
+
)
|
|
69
|
+
mcp_type: MCPType = field(
|
|
70
|
+
metadata={"description": "The type of FastMCP component to create."},
|
|
71
|
+
)
|
|
72
|
+
mcp_tags: set[str] = field(
|
|
73
|
+
default_factory=set,
|
|
74
|
+
metadata={
|
|
75
|
+
"description": "A set of tags to apply to the generated FastMCP component."
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Default route mapping: all routes become tools.
|
|
81
|
+
DEFAULT_ROUTE_MAPPINGS = [
|
|
82
|
+
RouteMap(mcp_type=MCPType.TOOL),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _determine_route_type(
|
|
87
|
+
route: HTTPRoute,
|
|
88
|
+
mappings: list[RouteMap],
|
|
89
|
+
) -> RouteMap:
|
|
90
|
+
"""Determine the FastMCP component type based on the route and mappings."""
|
|
91
|
+
for route_map in mappings:
|
|
92
|
+
if route_map.methods == "*" or route.method in route_map.methods:
|
|
93
|
+
if isinstance(route_map.pattern, Pattern):
|
|
94
|
+
pattern_matches = route_map.pattern.search(route.path)
|
|
95
|
+
else:
|
|
96
|
+
pattern_matches = re.search(route_map.pattern, route.path)
|
|
97
|
+
|
|
98
|
+
if pattern_matches:
|
|
99
|
+
if route_map.tags:
|
|
100
|
+
route_tags_set = set(route.tags or [])
|
|
101
|
+
if not route_map.tags.issubset(route_tags_set):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"Route {route.method} {route.path} mapped to {route_map.mcp_type.name}"
|
|
106
|
+
)
|
|
107
|
+
return route_map
|
|
108
|
+
|
|
109
|
+
return RouteMap(mcp_type=MCPType.TOOL)
|