fastmcp 2.7.0__py3-none-any.whl → 2.8.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/__init__.py +4 -1
- fastmcp/cli/cli.py +4 -3
- fastmcp/client/auth/oauth.py +1 -1
- fastmcp/client/client.py +3 -1
- fastmcp/client/transports.py +43 -37
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/prompt.py +10 -20
- fastmcp/prompts/prompt_manager.py +7 -4
- fastmcp/resources/resource.py +21 -26
- fastmcp/resources/resource_manager.py +3 -2
- fastmcp/resources/template.py +8 -16
- fastmcp/server/auth/providers/bearer_env.py +8 -11
- fastmcp/server/dependencies.py +1 -0
- fastmcp/server/http.py +2 -1
- fastmcp/server/openapi.py +65 -38
- fastmcp/server/proxy.py +27 -14
- fastmcp/server/server.py +323 -133
- fastmcp/settings.py +100 -37
- fastmcp/tools/__init__.py +2 -1
- fastmcp/tools/tool.py +116 -77
- fastmcp/tools/tool_manager.py +3 -2
- fastmcp/tools/tool_transform.py +665 -0
- fastmcp/utilities/components.py +55 -0
- fastmcp/utilities/exceptions.py +1 -1
- fastmcp/utilities/mcp_config.py +12 -4
- fastmcp/utilities/tests.py +3 -3
- fastmcp/utilities/types.py +0 -9
- {fastmcp-2.7.0.dist-info → fastmcp-2.8.0.dist-info}/METADATA +3 -1
- {fastmcp-2.7.0.dist-info → fastmcp-2.8.0.dist-info}/RECORD +32 -30
- {fastmcp-2.7.0.dist-info → fastmcp-2.8.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.7.0.dist-info → fastmcp-2.8.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.7.0.dist-info → fastmcp-2.8.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/dependencies.py
CHANGED
fastmcp/server/http.py
CHANGED
|
@@ -13,6 +13,7 @@ from mcp.server.auth.middleware.bearer_auth import (
|
|
|
13
13
|
from mcp.server.auth.routes import create_auth_routes
|
|
14
14
|
from mcp.server.lowlevel.server import LifespanResultT
|
|
15
15
|
from mcp.server.sse import SseServerTransport
|
|
16
|
+
from mcp.server.streamable_http import EventStore
|
|
16
17
|
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
17
18
|
from starlette.applications import Starlette
|
|
18
19
|
from starlette.middleware import Middleware
|
|
@@ -241,7 +242,7 @@ def create_sse_app(
|
|
|
241
242
|
def create_streamable_http_app(
|
|
242
243
|
server: FastMCP[LifespanResultT],
|
|
243
244
|
streamable_http_path: str,
|
|
244
|
-
event_store: None = None,
|
|
245
|
+
event_store: EventStore | None = None,
|
|
245
246
|
auth: OAuthProvider | None = None,
|
|
246
247
|
json_response: bool = False,
|
|
247
248
|
stateless_http: bool = False,
|
fastmcp/server/openapi.py
CHANGED
|
@@ -103,15 +103,27 @@ class RouteType(enum.Enum):
|
|
|
103
103
|
IGNORE = "IGNORE"
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
@dataclass
|
|
106
|
+
@dataclass(kw_only=True)
|
|
107
107
|
class RouteMap:
|
|
108
108
|
"""Mapping configuration for HTTP routes to FastMCP component types."""
|
|
109
109
|
|
|
110
110
|
methods: list[HttpMethod] | Literal["*"] = field(default="*")
|
|
111
111
|
pattern: Pattern[str] | str = field(default=r".*")
|
|
112
|
-
mcp_type: MCPType | None = field(default=None)
|
|
113
112
|
route_type: RouteType | MCPType | None = field(default=None)
|
|
114
|
-
tags: set[str] = field(
|
|
113
|
+
tags: set[str] = field(
|
|
114
|
+
default_factory=set,
|
|
115
|
+
metadata={"description": "A set of tags to match. All tags must match."},
|
|
116
|
+
)
|
|
117
|
+
mcp_type: MCPType | None = field(
|
|
118
|
+
default=None,
|
|
119
|
+
metadata={"description": "The type of FastMCP component to create."},
|
|
120
|
+
)
|
|
121
|
+
mcp_tags: set[str] = field(
|
|
122
|
+
default_factory=set,
|
|
123
|
+
metadata={
|
|
124
|
+
"description": "A set of tags to apply to the generated FastMCP component."
|
|
125
|
+
},
|
|
126
|
+
)
|
|
115
127
|
|
|
116
128
|
def __post_init__(self):
|
|
117
129
|
"""Validate and process the route map after initialization."""
|
|
@@ -155,23 +167,17 @@ class RouteMap:
|
|
|
155
167
|
self.route_type = self.mcp_type
|
|
156
168
|
|
|
157
169
|
|
|
158
|
-
# Default route
|
|
170
|
+
# Default route mapping: all routes become tools.
|
|
171
|
+
# Users can provide custom route_maps to override this behavior.
|
|
159
172
|
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),
|
|
173
|
+
RouteMap(mcp_type=MCPType.TOOL),
|
|
168
174
|
]
|
|
169
175
|
|
|
170
176
|
|
|
171
177
|
def _determine_route_type(
|
|
172
178
|
route: openapi.HTTPRoute,
|
|
173
179
|
mappings: list[RouteMap],
|
|
174
|
-
) ->
|
|
180
|
+
) -> RouteMap:
|
|
175
181
|
"""
|
|
176
182
|
Determines the FastMCP component type based on the route and mappings.
|
|
177
183
|
|
|
@@ -180,7 +186,7 @@ def _determine_route_type(
|
|
|
180
186
|
mappings: List of RouteMap objects in priority order
|
|
181
187
|
|
|
182
188
|
Returns:
|
|
183
|
-
|
|
189
|
+
The RouteMap that matches the route, or a catchall "Tool" RouteMap if no match is found.
|
|
184
190
|
"""
|
|
185
191
|
# Check mappings in priority order (first match wins)
|
|
186
192
|
for route_map in mappings:
|
|
@@ -207,10 +213,10 @@ def _determine_route_type(
|
|
|
207
213
|
logger.debug(
|
|
208
214
|
f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
|
|
209
215
|
)
|
|
210
|
-
return route_map
|
|
216
|
+
return route_map
|
|
211
217
|
|
|
212
218
|
# Default fallback
|
|
213
|
-
return MCPType.TOOL
|
|
219
|
+
return RouteMap(mcp_type=MCPType.TOOL)
|
|
214
220
|
|
|
215
221
|
|
|
216
222
|
class OpenAPITool(Tool):
|
|
@@ -223,19 +229,17 @@ class OpenAPITool(Tool):
|
|
|
223
229
|
name: str,
|
|
224
230
|
description: str,
|
|
225
231
|
parameters: dict[str, Any],
|
|
226
|
-
tags: set[str] =
|
|
232
|
+
tags: set[str] | None = None,
|
|
227
233
|
timeout: float | None = None,
|
|
228
234
|
annotations: ToolAnnotations | None = None,
|
|
229
|
-
exclude_args: list[str] | None = None,
|
|
230
235
|
serializer: Callable[[Any], str] | None = None,
|
|
231
236
|
):
|
|
232
237
|
super().__init__(
|
|
233
238
|
name=name,
|
|
234
239
|
description=description,
|
|
235
240
|
parameters=parameters,
|
|
236
|
-
tags=tags,
|
|
241
|
+
tags=tags or set(),
|
|
237
242
|
annotations=annotations,
|
|
238
|
-
exclude_args=exclude_args,
|
|
239
243
|
serializer=serializer,
|
|
240
244
|
)
|
|
241
245
|
self._client = client
|
|
@@ -688,6 +692,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
688
692
|
route_map_fn: RouteMapFn | None = None,
|
|
689
693
|
mcp_component_fn: ComponentFn | None = None,
|
|
690
694
|
mcp_names: dict[str, str] | None = None,
|
|
695
|
+
tags: set[str] | None = None,
|
|
691
696
|
timeout: float | None = None,
|
|
692
697
|
**settings: Any,
|
|
693
698
|
):
|
|
@@ -710,6 +715,8 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
710
715
|
operationId up to the first double underscore. If no operationId exists,
|
|
711
716
|
falls back to slugified summary or path-based naming.
|
|
712
717
|
All names are truncated to 56 characters maximum.
|
|
718
|
+
tags: Optional set of tags to add to all components. Components always receive any tags
|
|
719
|
+
from the route.
|
|
713
720
|
timeout: Optional timeout (in seconds) for all requests
|
|
714
721
|
**settings: Additional settings for FastMCP
|
|
715
722
|
"""
|
|
@@ -717,9 +724,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
717
724
|
|
|
718
725
|
self._client = client
|
|
719
726
|
self._timeout = timeout
|
|
720
|
-
self._route_map_fn = route_map_fn
|
|
721
727
|
self._mcp_component_fn = mcp_component_fn
|
|
722
|
-
self._mcp_names = mcp_names or {}
|
|
723
728
|
|
|
724
729
|
# Keep track of names to detect collisions
|
|
725
730
|
self._used_names = {
|
|
@@ -735,12 +740,16 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
735
740
|
route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS
|
|
736
741
|
for route in http_routes:
|
|
737
742
|
# Determine route type based on mappings or default rules
|
|
738
|
-
|
|
743
|
+
route_map = _determine_route_type(route, route_maps)
|
|
744
|
+
|
|
745
|
+
# TODO: remove this once RouteType is removed and mcp_type is typed as MCPType without | None
|
|
746
|
+
assert route_map.mcp_type is not None
|
|
747
|
+
route_type = route_map.mcp_type
|
|
739
748
|
|
|
740
749
|
# Call route_map_fn if provided
|
|
741
|
-
if
|
|
750
|
+
if route_map_fn is not None:
|
|
742
751
|
try:
|
|
743
|
-
result =
|
|
752
|
+
result = route_map_fn(route, route_type)
|
|
744
753
|
if result is not None:
|
|
745
754
|
route_type = result
|
|
746
755
|
logger.debug(
|
|
@@ -754,29 +763,32 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
754
763
|
)
|
|
755
764
|
|
|
756
765
|
# Generate a default name from the route
|
|
757
|
-
component_name = self._generate_default_name(route,
|
|
766
|
+
component_name = self._generate_default_name(route, mcp_names)
|
|
767
|
+
|
|
768
|
+
route_tags = set(route.tags) | route_map.mcp_tags | (tags or set())
|
|
758
769
|
|
|
759
770
|
if route_type == MCPType.TOOL:
|
|
760
|
-
self._create_openapi_tool(route, component_name)
|
|
771
|
+
self._create_openapi_tool(route, component_name, tags=route_tags)
|
|
761
772
|
elif route_type == MCPType.RESOURCE:
|
|
762
|
-
self._create_openapi_resource(route, component_name)
|
|
773
|
+
self._create_openapi_resource(route, component_name, tags=route_tags)
|
|
763
774
|
elif route_type == MCPType.RESOURCE_TEMPLATE:
|
|
764
|
-
self._create_openapi_template(route, component_name)
|
|
775
|
+
self._create_openapi_template(route, component_name, tags=route_tags)
|
|
765
776
|
elif route_type == MCPType.EXCLUDE:
|
|
766
777
|
logger.info(f"Excluding route: {route.method} {route.path}")
|
|
767
778
|
|
|
768
779
|
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
|
|
769
780
|
|
|
770
781
|
def _generate_default_name(
|
|
771
|
-
self, route: openapi.HTTPRoute,
|
|
782
|
+
self, route: openapi.HTTPRoute, mcp_names_map: dict[str, str] | None = None
|
|
772
783
|
) -> str:
|
|
773
784
|
"""Generate a default name from the route using the configured strategy."""
|
|
774
785
|
name = ""
|
|
786
|
+
mcp_names_map = mcp_names_map or {}
|
|
775
787
|
|
|
776
788
|
# First check if there's a custom mapping for this operationId
|
|
777
789
|
if route.operation_id:
|
|
778
|
-
if route.operation_id in
|
|
779
|
-
name =
|
|
790
|
+
if route.operation_id in mcp_names_map:
|
|
791
|
+
name = mcp_names_map[route.operation_id]
|
|
780
792
|
else:
|
|
781
793
|
# If there's a double underscore in the operationId, use the first part
|
|
782
794
|
name = route.operation_id.split("__")[0]
|
|
@@ -821,7 +833,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
821
833
|
|
|
822
834
|
return new_name
|
|
823
835
|
|
|
824
|
-
def _create_openapi_tool(
|
|
836
|
+
def _create_openapi_tool(
|
|
837
|
+
self,
|
|
838
|
+
route: openapi.HTTPRoute,
|
|
839
|
+
name: str,
|
|
840
|
+
tags: set[str],
|
|
841
|
+
):
|
|
825
842
|
"""Creates and registers an OpenAPITool with enhanced description."""
|
|
826
843
|
combined_schema = _combine_schemas(route)
|
|
827
844
|
|
|
@@ -848,7 +865,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
848
865
|
name=tool_name,
|
|
849
866
|
description=enhanced_description,
|
|
850
867
|
parameters=combined_schema,
|
|
851
|
-
tags=set(route.tags or []),
|
|
868
|
+
tags=set(route.tags or []) | tags,
|
|
852
869
|
timeout=self._timeout,
|
|
853
870
|
)
|
|
854
871
|
|
|
@@ -869,7 +886,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
869
886
|
f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
|
|
870
887
|
)
|
|
871
888
|
|
|
872
|
-
def _create_openapi_resource(
|
|
889
|
+
def _create_openapi_resource(
|
|
890
|
+
self,
|
|
891
|
+
route: openapi.HTTPRoute,
|
|
892
|
+
name: str,
|
|
893
|
+
tags: set[str],
|
|
894
|
+
):
|
|
873
895
|
"""Creates and registers an OpenAPIResource with enhanced description."""
|
|
874
896
|
# Get a unique resource name
|
|
875
897
|
resource_name = self._get_unique_name(name, "resource")
|
|
@@ -893,7 +915,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
893
915
|
uri=resource_uri,
|
|
894
916
|
name=resource_name,
|
|
895
917
|
description=enhanced_description,
|
|
896
|
-
tags=set(route.tags or []),
|
|
918
|
+
tags=set(route.tags or []) | tags,
|
|
897
919
|
timeout=self._timeout,
|
|
898
920
|
)
|
|
899
921
|
|
|
@@ -914,7 +936,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
914
936
|
f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
|
|
915
937
|
)
|
|
916
938
|
|
|
917
|
-
def _create_openapi_template(
|
|
939
|
+
def _create_openapi_template(
|
|
940
|
+
self,
|
|
941
|
+
route: openapi.HTTPRoute,
|
|
942
|
+
name: str,
|
|
943
|
+
tags: set[str],
|
|
944
|
+
):
|
|
918
945
|
"""Creates and registers an OpenAPIResourceTemplate with enhanced description."""
|
|
919
946
|
# Get a unique template name
|
|
920
947
|
template_name = self._get_unique_name(name, "resource_template")
|
|
@@ -967,7 +994,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
967
994
|
name=template_name,
|
|
968
995
|
description=enhanced_description,
|
|
969
996
|
parameters=template_params_schema,
|
|
970
|
-
tags=set(route.tags or []),
|
|
997
|
+
tags=set(route.tags or []) | tags,
|
|
971
998
|
timeout=self._timeout,
|
|
972
999
|
)
|
|
973
1000
|
|
fastmcp/server/proxy.py
CHANGED
|
@@ -186,8 +186,10 @@ class FastMCPProxy(FastMCP):
|
|
|
186
186
|
else:
|
|
187
187
|
raise e
|
|
188
188
|
for tool in client_tools:
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
# don't overwrite tools defined in the server
|
|
190
|
+
if tool.name not in tools:
|
|
191
|
+
tool_proxy = await ProxyTool.from_client(self.client, tool)
|
|
192
|
+
tools[tool_proxy.name] = tool_proxy
|
|
191
193
|
|
|
192
194
|
return tools
|
|
193
195
|
|
|
@@ -203,8 +205,12 @@ class FastMCPProxy(FastMCP):
|
|
|
203
205
|
else:
|
|
204
206
|
raise e
|
|
205
207
|
for resource in client_resources:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
# don't overwrite resources defined in the server
|
|
209
|
+
if str(resource.uri) not in resources:
|
|
210
|
+
resource_proxy = await ProxyResource.from_client(
|
|
211
|
+
self.client, resource
|
|
212
|
+
)
|
|
213
|
+
resources[str(resource_proxy.uri)] = resource_proxy
|
|
208
214
|
|
|
209
215
|
return resources
|
|
210
216
|
|
|
@@ -220,8 +226,12 @@ class FastMCPProxy(FastMCP):
|
|
|
220
226
|
else:
|
|
221
227
|
raise e
|
|
222
228
|
for template in client_templates:
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
# don't overwrite templates defined in the server
|
|
230
|
+
if template.uriTemplate not in templates:
|
|
231
|
+
template_proxy = await ProxyTemplate.from_client(
|
|
232
|
+
self.client, template
|
|
233
|
+
)
|
|
234
|
+
templates[template_proxy.uri_template] = template_proxy
|
|
225
235
|
|
|
226
236
|
return templates
|
|
227
237
|
|
|
@@ -237,24 +247,27 @@ class FastMCPProxy(FastMCP):
|
|
|
237
247
|
else:
|
|
238
248
|
raise e
|
|
239
249
|
for prompt in client_prompts:
|
|
240
|
-
|
|
241
|
-
|
|
250
|
+
# don't overwrite prompts defined in the server
|
|
251
|
+
if prompt.name not in prompts:
|
|
252
|
+
prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
|
|
253
|
+
prompts[prompt_proxy.name] = prompt_proxy
|
|
254
|
+
|
|
242
255
|
return prompts
|
|
243
256
|
|
|
244
|
-
async def
|
|
257
|
+
async def _call_tool(
|
|
245
258
|
self, key: str, arguments: dict[str, Any]
|
|
246
259
|
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
247
260
|
try:
|
|
248
|
-
result = await super().
|
|
261
|
+
result = await super()._call_tool(key, arguments)
|
|
249
262
|
return result
|
|
250
263
|
except NotFoundError:
|
|
251
264
|
async with self.client:
|
|
252
265
|
result = await self.client.call_tool(key, arguments)
|
|
253
266
|
return result
|
|
254
267
|
|
|
255
|
-
async def
|
|
268
|
+
async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
|
256
269
|
try:
|
|
257
|
-
result = await super().
|
|
270
|
+
result = await super()._read_resource(uri)
|
|
258
271
|
return result
|
|
259
272
|
except NotFoundError:
|
|
260
273
|
async with self.client:
|
|
@@ -270,11 +283,11 @@ class FastMCPProxy(FastMCP):
|
|
|
270
283
|
ReadResourceContents(content=content, mime_type=resource[0].mimeType)
|
|
271
284
|
]
|
|
272
285
|
|
|
273
|
-
async def
|
|
286
|
+
async def _get_prompt(
|
|
274
287
|
self, name: str, arguments: dict[str, Any] | None = None
|
|
275
288
|
) -> GetPromptResult:
|
|
276
289
|
try:
|
|
277
|
-
result = await super().
|
|
290
|
+
result = await super()._get_prompt(name, arguments)
|
|
278
291
|
return result
|
|
279
292
|
except NotFoundError:
|
|
280
293
|
async with self.client:
|