fastmcp 2.7.1__py3-none-any.whl → 2.8.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/__init__.py +32 -3
- fastmcp/cli/cli.py +3 -2
- fastmcp/client/auth/oauth.py +1 -1
- fastmcp/client/client.py +6 -5
- fastmcp/client/sampling.py +5 -9
- fastmcp/client/transports.py +42 -33
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/prompt.py +11 -21
- fastmcp/prompts/prompt_manager.py +13 -9
- fastmcp/resources/resource.py +21 -26
- fastmcp/resources/resource_manager.py +15 -12
- fastmcp/resources/template.py +8 -16
- fastmcp/server/auth/providers/bearer_env.py +8 -11
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/context.py +12 -10
- fastmcp/server/openapi.py +87 -57
- fastmcp/server/proxy.py +29 -20
- fastmcp/server/server.py +422 -206
- fastmcp/settings.py +113 -37
- fastmcp/tools/__init__.py +2 -1
- fastmcp/tools/tool.py +125 -85
- fastmcp/tools/tool_manager.py +12 -11
- fastmcp/tools/tool_transform.py +669 -0
- fastmcp/utilities/components.py +55 -0
- fastmcp/utilities/exceptions.py +1 -1
- fastmcp/utilities/mcp_config.py +1 -1
- fastmcp/utilities/tests.py +3 -3
- fastmcp/utilities/types.py +82 -14
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/METADATA +48 -26
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/RECORD +33 -31
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.7.1.dist-info → fastmcp-2.8.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/resources/template.py
CHANGED
|
@@ -5,12 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
from urllib.parse import unquote
|
|
10
10
|
|
|
11
11
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
12
12
|
from pydantic import (
|
|
13
|
-
BeforeValidator,
|
|
14
13
|
Field,
|
|
15
14
|
field_validator,
|
|
16
15
|
validate_call,
|
|
@@ -18,10 +17,9 @@ from pydantic import (
|
|
|
18
17
|
|
|
19
18
|
from fastmcp.resources.types import Resource
|
|
20
19
|
from fastmcp.server.dependencies import get_context
|
|
20
|
+
from fastmcp.utilities.components import FastMCPComponent
|
|
21
21
|
from fastmcp.utilities.json_schema import compress_schema
|
|
22
22
|
from fastmcp.utilities.types import (
|
|
23
|
-
FastMCPBaseModel,
|
|
24
|
-
_convert_set_defaults,
|
|
25
23
|
find_kwarg_by_type,
|
|
26
24
|
get_cached_typeadapter,
|
|
27
25
|
)
|
|
@@ -51,17 +49,12 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
|
|
|
51
49
|
return None
|
|
52
50
|
|
|
53
51
|
|
|
54
|
-
class ResourceTemplate(
|
|
52
|
+
class ResourceTemplate(FastMCPComponent):
|
|
55
53
|
"""A template for dynamically creating resources."""
|
|
56
54
|
|
|
57
55
|
uri_template: str = Field(
|
|
58
56
|
description="URI template with parameters (e.g. weather://{city}/current)"
|
|
59
57
|
)
|
|
60
|
-
name: str = Field(description="Name of the resource")
|
|
61
|
-
description: str | None = Field(description="Description of what the resource does")
|
|
62
|
-
tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
|
|
63
|
-
default_factory=set, description="Tags for the resource"
|
|
64
|
-
)
|
|
65
58
|
mime_type: str = Field(
|
|
66
59
|
default="text/plain", description="MIME type of the resource content"
|
|
67
60
|
)
|
|
@@ -77,6 +70,7 @@ class ResourceTemplate(FastMCPBaseModel):
|
|
|
77
70
|
description: str | None = None,
|
|
78
71
|
mime_type: str | None = None,
|
|
79
72
|
tags: set[str] | None = None,
|
|
73
|
+
enabled: bool | None = None,
|
|
80
74
|
) -> FunctionResourceTemplate:
|
|
81
75
|
return FunctionResourceTemplate.from_function(
|
|
82
76
|
fn=fn,
|
|
@@ -85,6 +79,7 @@ class ResourceTemplate(FastMCPBaseModel):
|
|
|
85
79
|
description=description,
|
|
86
80
|
mime_type=mime_type,
|
|
87
81
|
tags=tags,
|
|
82
|
+
enabled=enabled,
|
|
88
83
|
)
|
|
89
84
|
|
|
90
85
|
@field_validator("mime_type", mode="before")
|
|
@@ -120,14 +115,9 @@ class ResourceTemplate(FastMCPBaseModel):
|
|
|
120
115
|
description=self.description,
|
|
121
116
|
mime_type=self.mime_type,
|
|
122
117
|
tags=self.tags,
|
|
118
|
+
enabled=self.enabled,
|
|
123
119
|
)
|
|
124
120
|
|
|
125
|
-
def __eq__(self, other: object) -> bool:
|
|
126
|
-
if type(self) is not type(other):
|
|
127
|
-
return False
|
|
128
|
-
assert isinstance(other, type(self))
|
|
129
|
-
return self.model_dump() == other.model_dump()
|
|
130
|
-
|
|
131
121
|
def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate:
|
|
132
122
|
"""Convert the resource template to an MCPResourceTemplate."""
|
|
133
123
|
kwargs = {
|
|
@@ -168,6 +158,7 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
168
158
|
description: str | None = None,
|
|
169
159
|
mime_type: str | None = None,
|
|
170
160
|
tags: set[str] | None = None,
|
|
161
|
+
enabled: bool | None = None,
|
|
171
162
|
) -> FunctionResourceTemplate:
|
|
172
163
|
"""Create a template from a function."""
|
|
173
164
|
from fastmcp.server.context import Context
|
|
@@ -250,4 +241,5 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|
|
250
241
|
fn=fn,
|
|
251
242
|
parameters=parameters,
|
|
252
243
|
tags=tags or set(),
|
|
244
|
+
enabled=enabled if enabled is not None else True,
|
|
253
245
|
)
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
+
from types import EllipsisType
|
|
2
|
+
|
|
1
3
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
4
|
|
|
3
5
|
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
|
|
4
6
|
|
|
5
7
|
|
|
6
|
-
# Sentinel object to indicate that a setting is not set
|
|
7
|
-
class _NotSet:
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
|
|
11
8
|
class EnvBearerAuthProviderSettings(BaseSettings):
|
|
12
9
|
"""Settings for the BearerAuthProvider."""
|
|
13
10
|
|
|
@@ -33,11 +30,11 @@ class EnvBearerAuthProvider(BearerAuthProvider):
|
|
|
33
30
|
|
|
34
31
|
def __init__(
|
|
35
32
|
self,
|
|
36
|
-
public_key: str | None |
|
|
37
|
-
jwks_uri: str | None |
|
|
38
|
-
issuer: str | None |
|
|
39
|
-
audience: str | None |
|
|
40
|
-
required_scopes: list[str] | None |
|
|
33
|
+
public_key: str | None | EllipsisType = ...,
|
|
34
|
+
jwks_uri: str | None | EllipsisType = ...,
|
|
35
|
+
issuer: str | None | EllipsisType = ...,
|
|
36
|
+
audience: str | None | EllipsisType = ...,
|
|
37
|
+
required_scopes: list[str] | None | EllipsisType = ...,
|
|
41
38
|
):
|
|
42
39
|
"""
|
|
43
40
|
Initialize the provider.
|
|
@@ -57,6 +54,6 @@ class EnvBearerAuthProvider(BearerAuthProvider):
|
|
|
57
54
|
"required_scopes": required_scopes,
|
|
58
55
|
}
|
|
59
56
|
settings = EnvBearerAuthProviderSettings(
|
|
60
|
-
**{k: v for k, v in kwargs.items() if v is not
|
|
57
|
+
**{k: v for k, v in kwargs.items() if v is not ...}
|
|
61
58
|
)
|
|
62
59
|
super().__init__(**settings.model_dump())
|
|
@@ -184,7 +184,7 @@ class InMemoryOAuthProvider(OAuthProvider):
|
|
|
184
184
|
|
|
185
185
|
return OAuthToken(
|
|
186
186
|
access_token=access_token_value,
|
|
187
|
-
token_type="
|
|
187
|
+
token_type="Bearer",
|
|
188
188
|
expires_in=DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,
|
|
189
189
|
refresh_token=refresh_token_value,
|
|
190
190
|
scope=" ".join(authorization_code.scopes),
|
|
@@ -254,7 +254,7 @@ class InMemoryOAuthProvider(OAuthProvider):
|
|
|
254
254
|
|
|
255
255
|
return OAuthToken(
|
|
256
256
|
access_token=new_access_token_value,
|
|
257
|
-
token_type="
|
|
257
|
+
token_type="Bearer",
|
|
258
258
|
expires_in=DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS,
|
|
259
259
|
refresh_token=new_refresh_token_value,
|
|
260
260
|
scope=" ".join(scopes),
|
fastmcp/server/context.py
CHANGED
|
@@ -11,7 +11,6 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
|
11
11
|
from mcp.shared.context import RequestContext
|
|
12
12
|
from mcp.types import (
|
|
13
13
|
CreateMessageResult,
|
|
14
|
-
ImageContent,
|
|
15
14
|
ModelHint,
|
|
16
15
|
ModelPreferences,
|
|
17
16
|
Root,
|
|
@@ -22,8 +21,10 @@ from pydantic.networks import AnyUrl
|
|
|
22
21
|
from starlette.requests import Request
|
|
23
22
|
|
|
24
23
|
import fastmcp.server.dependencies
|
|
24
|
+
from fastmcp import settings
|
|
25
25
|
from fastmcp.server.server import FastMCP
|
|
26
26
|
from fastmcp.utilities.logging import get_logger
|
|
27
|
+
from fastmcp.utilities.types import MCPContent
|
|
27
28
|
|
|
28
29
|
logger = get_logger(__name__)
|
|
29
30
|
|
|
@@ -203,7 +204,7 @@ class Context:
|
|
|
203
204
|
temperature: float | None = None,
|
|
204
205
|
max_tokens: int | None = None,
|
|
205
206
|
model_preferences: ModelPreferences | str | list[str] | None = None,
|
|
206
|
-
) ->
|
|
207
|
+
) -> MCPContent:
|
|
207
208
|
"""
|
|
208
209
|
Send a sampling request to the client and await the response.
|
|
209
210
|
|
|
@@ -242,14 +243,15 @@ class Context:
|
|
|
242
243
|
def get_http_request(self) -> Request:
|
|
243
244
|
"""Get the active starlette request."""
|
|
244
245
|
|
|
245
|
-
#
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
# Deprecated in 2.2.11
|
|
247
|
+
if settings.deprecation_warnings:
|
|
248
|
+
warnings.warn(
|
|
249
|
+
"Context.get_http_request() is deprecated and will be removed in a future version. "
|
|
250
|
+
"Use get_http_request() from fastmcp.server.dependencies instead. "
|
|
251
|
+
"See https://gofastmcp.com/patterns/http-requests for more details.",
|
|
252
|
+
DeprecationWarning,
|
|
253
|
+
stacklevel=2,
|
|
254
|
+
)
|
|
253
255
|
|
|
254
256
|
return fastmcp.server.dependencies.get_http_request()
|
|
255
257
|
|
fastmcp/server/openapi.py
CHANGED
|
@@ -13,9 +13,10 @@ from re import Pattern
|
|
|
13
13
|
from typing import TYPE_CHECKING, Any, Literal
|
|
14
14
|
|
|
15
15
|
import httpx
|
|
16
|
-
from mcp.types import
|
|
16
|
+
from mcp.types import ToolAnnotations
|
|
17
17
|
from pydantic.networks import AnyUrl
|
|
18
18
|
|
|
19
|
+
import fastmcp
|
|
19
20
|
from fastmcp.exceptions import ToolError
|
|
20
21
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
21
22
|
from fastmcp.server.dependencies import get_http_headers
|
|
@@ -28,6 +29,7 @@ from fastmcp.utilities.openapi import (
|
|
|
28
29
|
_combine_schemas,
|
|
29
30
|
format_description_with_responses,
|
|
30
31
|
)
|
|
32
|
+
from fastmcp.utilities.types import MCPContent
|
|
31
33
|
|
|
32
34
|
if TYPE_CHECKING:
|
|
33
35
|
from fastmcp.server import Context
|
|
@@ -103,41 +105,56 @@ class RouteType(enum.Enum):
|
|
|
103
105
|
IGNORE = "IGNORE"
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
@dataclass
|
|
108
|
+
@dataclass(kw_only=True)
|
|
107
109
|
class RouteMap:
|
|
108
110
|
"""Mapping configuration for HTTP routes to FastMCP component types."""
|
|
109
111
|
|
|
110
112
|
methods: list[HttpMethod] | Literal["*"] = field(default="*")
|
|
111
113
|
pattern: Pattern[str] | str = field(default=r".*")
|
|
112
|
-
mcp_type: MCPType | None = field(default=None)
|
|
113
114
|
route_type: RouteType | MCPType | None = field(default=None)
|
|
114
|
-
tags: set[str] = field(
|
|
115
|
+
tags: set[str] = field(
|
|
116
|
+
default_factory=set,
|
|
117
|
+
metadata={"description": "A set of tags to match. All tags must match."},
|
|
118
|
+
)
|
|
119
|
+
mcp_type: MCPType | None = field(
|
|
120
|
+
default=None,
|
|
121
|
+
metadata={"description": "The type of FastMCP component to create."},
|
|
122
|
+
)
|
|
123
|
+
mcp_tags: set[str] = field(
|
|
124
|
+
default_factory=set,
|
|
125
|
+
metadata={
|
|
126
|
+
"description": "A set of tags to apply to the generated FastMCP component."
|
|
127
|
+
},
|
|
128
|
+
)
|
|
115
129
|
|
|
116
130
|
def __post_init__(self):
|
|
117
131
|
"""Validate and process the route map after initialization."""
|
|
118
132
|
# Handle backward compatibility for route_type, deprecated in 2.5.0
|
|
119
133
|
if self.mcp_type is None and self.route_type is not None:
|
|
120
|
-
|
|
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):
|
|
134
|
+
if fastmcp.settings.deprecation_warnings:
|
|
127
135
|
warnings.warn(
|
|
128
|
-
"The
|
|
129
|
-
"Use MCPType
|
|
136
|
+
"The 'route_type' parameter is deprecated and will be removed in a future version. "
|
|
137
|
+
"Use 'mcp_type' instead with the appropriate MCPType value.",
|
|
130
138
|
DeprecationWarning,
|
|
131
139
|
stacklevel=2,
|
|
132
140
|
)
|
|
141
|
+
if isinstance(self.route_type, RouteType):
|
|
142
|
+
if fastmcp.settings.deprecation_warnings:
|
|
143
|
+
warnings.warn(
|
|
144
|
+
"The RouteType class is deprecated and will be removed in a future version. "
|
|
145
|
+
"Use MCPType instead.",
|
|
146
|
+
DeprecationWarning,
|
|
147
|
+
stacklevel=2,
|
|
148
|
+
)
|
|
133
149
|
# Check for the deprecated IGNORE value
|
|
134
150
|
if self.route_type == RouteType.IGNORE:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
if fastmcp.settings.deprecation_warnings:
|
|
152
|
+
warnings.warn(
|
|
153
|
+
"RouteType.IGNORE is deprecated and will be removed in a future version. "
|
|
154
|
+
"Use MCPType.EXCLUDE instead.",
|
|
155
|
+
DeprecationWarning,
|
|
156
|
+
stacklevel=2,
|
|
157
|
+
)
|
|
141
158
|
|
|
142
159
|
# Convert from RouteType to MCPType if needed
|
|
143
160
|
if isinstance(self.route_type, RouteType):
|
|
@@ -155,23 +172,17 @@ class RouteMap:
|
|
|
155
172
|
self.route_type = self.mcp_type
|
|
156
173
|
|
|
157
174
|
|
|
158
|
-
# Default route
|
|
175
|
+
# Default route mapping: all routes become tools.
|
|
176
|
+
# Users can provide custom route_maps to override this behavior.
|
|
159
177
|
DEFAULT_ROUTE_MAPPINGS = [
|
|
160
|
-
|
|
161
|
-
RouteMap(
|
|
162
|
-
methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE
|
|
163
|
-
),
|
|
164
|
-
# GET requests without path parameters go to Resource
|
|
165
|
-
RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
|
|
166
|
-
# All other HTTP methods go to Tool
|
|
167
|
-
RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL),
|
|
178
|
+
RouteMap(mcp_type=MCPType.TOOL),
|
|
168
179
|
]
|
|
169
180
|
|
|
170
181
|
|
|
171
182
|
def _determine_route_type(
|
|
172
183
|
route: openapi.HTTPRoute,
|
|
173
184
|
mappings: list[RouteMap],
|
|
174
|
-
) ->
|
|
185
|
+
) -> RouteMap:
|
|
175
186
|
"""
|
|
176
187
|
Determines the FastMCP component type based on the route and mappings.
|
|
177
188
|
|
|
@@ -180,7 +191,7 @@ def _determine_route_type(
|
|
|
180
191
|
mappings: List of RouteMap objects in priority order
|
|
181
192
|
|
|
182
193
|
Returns:
|
|
183
|
-
|
|
194
|
+
The RouteMap that matches the route, or a catchall "Tool" RouteMap if no match is found.
|
|
184
195
|
"""
|
|
185
196
|
# Check mappings in priority order (first match wins)
|
|
186
197
|
for route_map in mappings:
|
|
@@ -207,10 +218,10 @@ def _determine_route_type(
|
|
|
207
218
|
logger.debug(
|
|
208
219
|
f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
|
|
209
220
|
)
|
|
210
|
-
return route_map
|
|
221
|
+
return route_map
|
|
211
222
|
|
|
212
223
|
# Default fallback
|
|
213
|
-
return MCPType.TOOL
|
|
224
|
+
return RouteMap(mcp_type=MCPType.TOOL)
|
|
214
225
|
|
|
215
226
|
|
|
216
227
|
class OpenAPITool(Tool):
|
|
@@ -223,19 +234,17 @@ class OpenAPITool(Tool):
|
|
|
223
234
|
name: str,
|
|
224
235
|
description: str,
|
|
225
236
|
parameters: dict[str, Any],
|
|
226
|
-
tags: set[str] =
|
|
237
|
+
tags: set[str] | None = None,
|
|
227
238
|
timeout: float | None = None,
|
|
228
239
|
annotations: ToolAnnotations | None = None,
|
|
229
|
-
exclude_args: list[str] | None = None,
|
|
230
240
|
serializer: Callable[[Any], str] | None = None,
|
|
231
241
|
):
|
|
232
242
|
super().__init__(
|
|
233
243
|
name=name,
|
|
234
244
|
description=description,
|
|
235
245
|
parameters=parameters,
|
|
236
|
-
tags=tags,
|
|
246
|
+
tags=tags or set(),
|
|
237
247
|
annotations=annotations,
|
|
238
|
-
exclude_args=exclude_args,
|
|
239
248
|
serializer=serializer,
|
|
240
249
|
)
|
|
241
250
|
self._client = client
|
|
@@ -246,9 +255,7 @@ class OpenAPITool(Tool):
|
|
|
246
255
|
"""Custom representation to prevent recursion errors when printing."""
|
|
247
256
|
return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
|
|
248
257
|
|
|
249
|
-
async def run(
|
|
250
|
-
self, arguments: dict[str, Any]
|
|
251
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
258
|
+
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
252
259
|
"""Execute the HTTP request based on the route configuration."""
|
|
253
260
|
|
|
254
261
|
# Prepare URL
|
|
@@ -688,6 +695,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
688
695
|
route_map_fn: RouteMapFn | None = None,
|
|
689
696
|
mcp_component_fn: ComponentFn | None = None,
|
|
690
697
|
mcp_names: dict[str, str] | None = None,
|
|
698
|
+
tags: set[str] | None = None,
|
|
691
699
|
timeout: float | None = None,
|
|
692
700
|
**settings: Any,
|
|
693
701
|
):
|
|
@@ -710,6 +718,8 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
710
718
|
operationId up to the first double underscore. If no operationId exists,
|
|
711
719
|
falls back to slugified summary or path-based naming.
|
|
712
720
|
All names are truncated to 56 characters maximum.
|
|
721
|
+
tags: Optional set of tags to add to all components. Components always receive any tags
|
|
722
|
+
from the route.
|
|
713
723
|
timeout: Optional timeout (in seconds) for all requests
|
|
714
724
|
**settings: Additional settings for FastMCP
|
|
715
725
|
"""
|
|
@@ -717,9 +727,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
717
727
|
|
|
718
728
|
self._client = client
|
|
719
729
|
self._timeout = timeout
|
|
720
|
-
self._route_map_fn = route_map_fn
|
|
721
730
|
self._mcp_component_fn = mcp_component_fn
|
|
722
|
-
self._mcp_names = mcp_names or {}
|
|
723
731
|
|
|
724
732
|
# Keep track of names to detect collisions
|
|
725
733
|
self._used_names = {
|
|
@@ -735,12 +743,16 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
735
743
|
route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS
|
|
736
744
|
for route in http_routes:
|
|
737
745
|
# Determine route type based on mappings or default rules
|
|
738
|
-
|
|
746
|
+
route_map = _determine_route_type(route, route_maps)
|
|
747
|
+
|
|
748
|
+
# TODO: remove this once RouteType is removed and mcp_type is typed as MCPType without | None
|
|
749
|
+
assert route_map.mcp_type is not None
|
|
750
|
+
route_type = route_map.mcp_type
|
|
739
751
|
|
|
740
752
|
# Call route_map_fn if provided
|
|
741
|
-
if
|
|
753
|
+
if route_map_fn is not None:
|
|
742
754
|
try:
|
|
743
|
-
result =
|
|
755
|
+
result = route_map_fn(route, route_type)
|
|
744
756
|
if result is not None:
|
|
745
757
|
route_type = result
|
|
746
758
|
logger.debug(
|
|
@@ -754,29 +766,32 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
754
766
|
)
|
|
755
767
|
|
|
756
768
|
# Generate a default name from the route
|
|
757
|
-
component_name = self._generate_default_name(route,
|
|
769
|
+
component_name = self._generate_default_name(route, mcp_names)
|
|
770
|
+
|
|
771
|
+
route_tags = set(route.tags) | route_map.mcp_tags | (tags or set())
|
|
758
772
|
|
|
759
773
|
if route_type == MCPType.TOOL:
|
|
760
|
-
self._create_openapi_tool(route, component_name)
|
|
774
|
+
self._create_openapi_tool(route, component_name, tags=route_tags)
|
|
761
775
|
elif route_type == MCPType.RESOURCE:
|
|
762
|
-
self._create_openapi_resource(route, component_name)
|
|
776
|
+
self._create_openapi_resource(route, component_name, tags=route_tags)
|
|
763
777
|
elif route_type == MCPType.RESOURCE_TEMPLATE:
|
|
764
|
-
self._create_openapi_template(route, component_name)
|
|
778
|
+
self._create_openapi_template(route, component_name, tags=route_tags)
|
|
765
779
|
elif route_type == MCPType.EXCLUDE:
|
|
766
780
|
logger.info(f"Excluding route: {route.method} {route.path}")
|
|
767
781
|
|
|
768
782
|
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
|
|
769
783
|
|
|
770
784
|
def _generate_default_name(
|
|
771
|
-
self, route: openapi.HTTPRoute,
|
|
785
|
+
self, route: openapi.HTTPRoute, mcp_names_map: dict[str, str] | None = None
|
|
772
786
|
) -> str:
|
|
773
787
|
"""Generate a default name from the route using the configured strategy."""
|
|
774
788
|
name = ""
|
|
789
|
+
mcp_names_map = mcp_names_map or {}
|
|
775
790
|
|
|
776
791
|
# First check if there's a custom mapping for this operationId
|
|
777
792
|
if route.operation_id:
|
|
778
|
-
if route.operation_id in
|
|
779
|
-
name =
|
|
793
|
+
if route.operation_id in mcp_names_map:
|
|
794
|
+
name = mcp_names_map[route.operation_id]
|
|
780
795
|
else:
|
|
781
796
|
# If there's a double underscore in the operationId, use the first part
|
|
782
797
|
name = route.operation_id.split("__")[0]
|
|
@@ -821,7 +836,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
821
836
|
|
|
822
837
|
return new_name
|
|
823
838
|
|
|
824
|
-
def _create_openapi_tool(
|
|
839
|
+
def _create_openapi_tool(
|
|
840
|
+
self,
|
|
841
|
+
route: openapi.HTTPRoute,
|
|
842
|
+
name: str,
|
|
843
|
+
tags: set[str],
|
|
844
|
+
):
|
|
825
845
|
"""Creates and registers an OpenAPITool with enhanced description."""
|
|
826
846
|
combined_schema = _combine_schemas(route)
|
|
827
847
|
|
|
@@ -848,7 +868,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
848
868
|
name=tool_name,
|
|
849
869
|
description=enhanced_description,
|
|
850
870
|
parameters=combined_schema,
|
|
851
|
-
tags=set(route.tags or []),
|
|
871
|
+
tags=set(route.tags or []) | tags,
|
|
852
872
|
timeout=self._timeout,
|
|
853
873
|
)
|
|
854
874
|
|
|
@@ -869,7 +889,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
869
889
|
f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
|
|
870
890
|
)
|
|
871
891
|
|
|
872
|
-
def _create_openapi_resource(
|
|
892
|
+
def _create_openapi_resource(
|
|
893
|
+
self,
|
|
894
|
+
route: openapi.HTTPRoute,
|
|
895
|
+
name: str,
|
|
896
|
+
tags: set[str],
|
|
897
|
+
):
|
|
873
898
|
"""Creates and registers an OpenAPIResource with enhanced description."""
|
|
874
899
|
# Get a unique resource name
|
|
875
900
|
resource_name = self._get_unique_name(name, "resource")
|
|
@@ -893,7 +918,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
893
918
|
uri=resource_uri,
|
|
894
919
|
name=resource_name,
|
|
895
920
|
description=enhanced_description,
|
|
896
|
-
tags=set(route.tags or []),
|
|
921
|
+
tags=set(route.tags or []) | tags,
|
|
897
922
|
timeout=self._timeout,
|
|
898
923
|
)
|
|
899
924
|
|
|
@@ -914,7 +939,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
914
939
|
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
|
|
915
940
|
)
|
|
916
941
|
|
|
917
|
-
def _create_openapi_template(
|
|
942
|
+
def _create_openapi_template(
|
|
943
|
+
self,
|
|
944
|
+
route: openapi.HTTPRoute,
|
|
945
|
+
name: str,
|
|
946
|
+
tags: set[str],
|
|
947
|
+
):
|
|
918
948
|
"""Creates and registers an OpenAPIResourceTemplate with enhanced description."""
|
|
919
949
|
# Get a unique template name
|
|
920
950
|
template_name = self._get_unique_name(name, "resource_template")
|
|
@@ -967,7 +997,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
967
997
|
name=template_name,
|
|
968
998
|
description=enhanced_description,
|
|
969
999
|
parameters=template_params_schema,
|
|
970
|
-
tags=set(route.tags or []),
|
|
1000
|
+
tags=set(route.tags or []) | tags,
|
|
971
1001
|
timeout=self._timeout,
|
|
972
1002
|
)
|
|
973
1003
|
|
fastmcp/server/proxy.py
CHANGED
|
@@ -9,10 +9,7 @@ from mcp.shared.exceptions import McpError
|
|
|
9
9
|
from mcp.types import (
|
|
10
10
|
METHOD_NOT_FOUND,
|
|
11
11
|
BlobResourceContents,
|
|
12
|
-
EmbeddedResource,
|
|
13
12
|
GetPromptResult,
|
|
14
|
-
ImageContent,
|
|
15
|
-
TextContent,
|
|
16
13
|
TextResourceContents,
|
|
17
14
|
)
|
|
18
15
|
from pydantic.networks import AnyUrl
|
|
@@ -25,6 +22,7 @@ from fastmcp.server.context import Context
|
|
|
25
22
|
from fastmcp.server.server import FastMCP
|
|
26
23
|
from fastmcp.tools.tool import Tool
|
|
27
24
|
from fastmcp.utilities.logging import get_logger
|
|
25
|
+
from fastmcp.utilities.types import MCPContent
|
|
28
26
|
|
|
29
27
|
if TYPE_CHECKING:
|
|
30
28
|
from fastmcp.server import Context
|
|
@@ -50,7 +48,7 @@ class ProxyTool(Tool):
|
|
|
50
48
|
self,
|
|
51
49
|
arguments: dict[str, Any],
|
|
52
50
|
context: Context | None = None,
|
|
53
|
-
) -> list[
|
|
51
|
+
) -> list[MCPContent]:
|
|
54
52
|
# the client context manager will swallow any exceptions inside a TaskGroup
|
|
55
53
|
# so we return the raw result and raise an exception ourselves
|
|
56
54
|
async with self._client:
|
|
@@ -186,8 +184,10 @@ class FastMCPProxy(FastMCP):
|
|
|
186
184
|
else:
|
|
187
185
|
raise e
|
|
188
186
|
for tool in client_tools:
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
# don't overwrite tools defined in the server
|
|
188
|
+
if tool.name not in tools:
|
|
189
|
+
tool_proxy = await ProxyTool.from_client(self.client, tool)
|
|
190
|
+
tools[tool_proxy.name] = tool_proxy
|
|
191
191
|
|
|
192
192
|
return tools
|
|
193
193
|
|
|
@@ -203,8 +203,12 @@ class FastMCPProxy(FastMCP):
|
|
|
203
203
|
else:
|
|
204
204
|
raise e
|
|
205
205
|
for resource in client_resources:
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
# don't overwrite resources defined in the server
|
|
207
|
+
if str(resource.uri) not in resources:
|
|
208
|
+
resource_proxy = await ProxyResource.from_client(
|
|
209
|
+
self.client, resource
|
|
210
|
+
)
|
|
211
|
+
resources[str(resource_proxy.uri)] = resource_proxy
|
|
208
212
|
|
|
209
213
|
return resources
|
|
210
214
|
|
|
@@ -220,8 +224,12 @@ class FastMCPProxy(FastMCP):
|
|
|
220
224
|
else:
|
|
221
225
|
raise e
|
|
222
226
|
for template in client_templates:
|
|
223
|
-
|
|
224
|
-
|
|
227
|
+
# don't overwrite templates defined in the server
|
|
228
|
+
if template.uriTemplate not in templates:
|
|
229
|
+
template_proxy = await ProxyTemplate.from_client(
|
|
230
|
+
self.client, template
|
|
231
|
+
)
|
|
232
|
+
templates[template_proxy.uri_template] = template_proxy
|
|
225
233
|
|
|
226
234
|
return templates
|
|
227
235
|
|
|
@@ -237,24 +245,25 @@ class FastMCPProxy(FastMCP):
|
|
|
237
245
|
else:
|
|
238
246
|
raise e
|
|
239
247
|
for prompt in client_prompts:
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
# don't overwrite prompts defined in the server
|
|
249
|
+
if prompt.name not in prompts:
|
|
250
|
+
prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
|
|
251
|
+
prompts[prompt_proxy.name] = prompt_proxy
|
|
252
|
+
|
|
242
253
|
return prompts
|
|
243
254
|
|
|
244
|
-
async def
|
|
245
|
-
self, key: str, arguments: dict[str, Any]
|
|
246
|
-
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
255
|
+
async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
|
|
247
256
|
try:
|
|
248
|
-
result = await super().
|
|
257
|
+
result = await super()._call_tool(key, arguments)
|
|
249
258
|
return result
|
|
250
259
|
except NotFoundError:
|
|
251
260
|
async with self.client:
|
|
252
261
|
result = await self.client.call_tool(key, arguments)
|
|
253
262
|
return result
|
|
254
263
|
|
|
255
|
-
async def
|
|
264
|
+
async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
256
265
|
try:
|
|
257
|
-
result = await super().
|
|
266
|
+
result = await super()._read_resource(uri)
|
|
258
267
|
return result
|
|
259
268
|
except NotFoundError:
|
|
260
269
|
async with self.client:
|
|
@@ -270,11 +279,11 @@ class FastMCPProxy(FastMCP):
|
|
|
270
279
|
ReadResourceContents(content=content, mime_type=resource[0].mimeType)
|
|
271
280
|
]
|
|
272
281
|
|
|
273
|
-
async def
|
|
282
|
+
async def _get_prompt(
|
|
274
283
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
275
284
|
) -> GetPromptResult:
|
|
276
285
|
try:
|
|
277
|
-
result = await super().
|
|
286
|
+
result = await super()._get_prompt(name, arguments)
|
|
278
287
|
return result
|
|
279
288
|
except NotFoundError:
|
|
280
289
|
async with self.client:
|