fastmcp 2.12.5__py3-none-any.whl → 2.14.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 +2 -23
- fastmcp/cli/__init__.py +0 -3
- fastmcp/cli/__main__.py +5 -0
- fastmcp/cli/cli.py +19 -33
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/install/shared.py +0 -15
- fastmcp/cli/run.py +13 -8
- fastmcp/cli/tasks.py +110 -0
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +123 -225
- fastmcp/client/client.py +697 -95
- fastmcp/client/elicitation.py +11 -5
- fastmcp/client/logging.py +18 -14
- fastmcp/client/messages.py +7 -5
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/roots.py +2 -1
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/tasks.py +614 -0
- fastmcp/client/transports.py +117 -30
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +10 -26
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/dependencies.py +25 -0
- fastmcp/experimental/sampling/handlers/openai.py +3 -3
- fastmcp/experimental/server/openapi/__init__.py +20 -21
- fastmcp/experimental/utilities/openapi/__init__.py +16 -47
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +54 -51
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +43 -21
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +161 -61
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -14
- fastmcp/server/auth/auth.py +197 -46
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1469 -298
- fastmcp/server/auth/oidc_proxy.py +91 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +29 -5
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +236 -116
- fastmcp/server/dependencies.py +503 -18
- fastmcp/server/elicitation.py +286 -48
- fastmcp/server/event_store.py +177 -0
- fastmcp/server/http.py +71 -20
- fastmcp/server/low_level.py +165 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi/__init__.py +35 -0
- fastmcp/{experimental/server → server}/openapi/components.py +15 -10
- fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
- fastmcp/{experimental/server → server}/openapi/server.py +6 -5
- fastmcp/server/proxy.py +72 -48
- fastmcp/server/server.py +1415 -733
- fastmcp/server/tasks/__init__.py +21 -0
- fastmcp/server/tasks/capabilities.py +22 -0
- fastmcp/server/tasks/config.py +89 -0
- fastmcp/server/tasks/converters.py +205 -0
- fastmcp/server/tasks/handlers.py +356 -0
- fastmcp/server/tasks/keys.py +93 -0
- fastmcp/server/tasks/protocol.py +355 -0
- fastmcp/server/tasks/subscriptions.py +205 -0
- fastmcp/settings.py +125 -113
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +138 -55
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -21
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +10 -5
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +8 -8
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_config.py +1 -2
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
- fastmcp/utilities/openapi/__init__.py +63 -0
- fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
- fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
- fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
- fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
- fastmcp/utilities/tests.py +92 -5
- fastmcp/utilities/types.py +86 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
- fastmcp-2.14.0.dist-info/RECORD +156 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/server/auth/providers/bearer.py +0 -25
- fastmcp/server/openapi.py +0 -1083
- fastmcp/utilities/openapi.py +0 -1568
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- fastmcp/{experimental/server → server}/openapi/README.md +0 -0
- fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
- fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
import time
|
|
5
6
|
from collections.abc import Callable
|
|
6
7
|
from logging import Logger
|
|
7
8
|
from typing import Any
|
|
@@ -45,21 +46,21 @@ class BaseLoggingMiddleware(Middleware):
|
|
|
45
46
|
|
|
46
47
|
return payload
|
|
47
48
|
|
|
48
|
-
def _format_message(self, message: dict[str, str | int]) -> str:
|
|
49
|
+
def _format_message(self, message: dict[str, str | int | float]) -> str:
|
|
49
50
|
"""Format a message for logging."""
|
|
50
51
|
if self.structured_logging:
|
|
51
52
|
return json.dumps(message)
|
|
52
53
|
else:
|
|
53
54
|
return " ".join([f"{k}={v}" for k, v in message.items()])
|
|
54
55
|
|
|
55
|
-
def _get_timestamp_from_context(self, context: MiddlewareContext[Any]) -> str:
|
|
56
|
-
"""Get a timestamp from the context."""
|
|
57
|
-
return context.timestamp.isoformat()
|
|
58
|
-
|
|
59
56
|
def _create_before_message(
|
|
60
|
-
self, context: MiddlewareContext[Any]
|
|
61
|
-
) -> dict[str, str | int]:
|
|
62
|
-
message =
|
|
57
|
+
self, context: MiddlewareContext[Any]
|
|
58
|
+
) -> dict[str, str | int | float]:
|
|
59
|
+
message = {
|
|
60
|
+
"event": context.type + "_start",
|
|
61
|
+
"method": context.method or "unknown",
|
|
62
|
+
"source": context.source,
|
|
63
|
+
}
|
|
63
64
|
|
|
64
65
|
if (
|
|
65
66
|
self.include_payloads
|
|
@@ -85,57 +86,61 @@ class BaseLoggingMiddleware(Middleware):
|
|
|
85
86
|
|
|
86
87
|
return message
|
|
87
88
|
|
|
88
|
-
def
|
|
89
|
-
self, context: MiddlewareContext[Any], event: str
|
|
90
|
-
) -> dict[str, str | int]:
|
|
91
|
-
return self._create_base_message(context, event)
|
|
92
|
-
|
|
93
|
-
def _create_base_message(
|
|
89
|
+
def _create_error_message(
|
|
94
90
|
self,
|
|
95
91
|
context: MiddlewareContext[Any],
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
start_time: float,
|
|
93
|
+
error: Exception,
|
|
94
|
+
) -> dict[str, str | int | float]:
|
|
95
|
+
duration_ms: float = _get_duration_ms(start_time)
|
|
96
|
+
message = {
|
|
97
|
+
"event": context.type + "_error",
|
|
98
|
+
"method": context.method or "unknown",
|
|
99
|
+
"source": context.source,
|
|
100
|
+
"duration_ms": duration_ms,
|
|
101
|
+
"error": str(object=error),
|
|
102
|
+
}
|
|
103
|
+
return message
|
|
99
104
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
def _create_after_message(
|
|
106
|
+
self,
|
|
107
|
+
context: MiddlewareContext[Any],
|
|
108
|
+
start_time: float,
|
|
109
|
+
) -> dict[str, str | int | float]:
|
|
110
|
+
duration_ms: float = _get_duration_ms(start_time)
|
|
111
|
+
message = {
|
|
112
|
+
"event": context.type + "_success",
|
|
103
113
|
"method": context.method or "unknown",
|
|
104
|
-
"type": context.type,
|
|
105
114
|
"source": context.source,
|
|
115
|
+
"duration_ms": duration_ms,
|
|
106
116
|
}
|
|
117
|
+
return message
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
def _log_message(
|
|
120
|
+
self, message: dict[str, str | int | float], log_level: int | None = None
|
|
121
|
+
):
|
|
122
|
+
self.logger.log(log_level or self.log_level, self._format_message(message))
|
|
109
123
|
|
|
110
124
|
async def on_message(
|
|
111
125
|
self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
|
|
112
126
|
) -> Any:
|
|
113
|
-
"""Log
|
|
127
|
+
"""Log messages for configured methods."""
|
|
114
128
|
|
|
115
129
|
if self.methods and context.method not in self.methods:
|
|
116
130
|
return await call_next(context)
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
context, "request_start"
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
formatted_message = self._format_message(request_start_log_message)
|
|
123
|
-
self.logger.log(self.log_level, f"Processing message: {formatted_message}")
|
|
132
|
+
self._log_message(self._create_before_message(context))
|
|
124
133
|
|
|
134
|
+
start_time = time.perf_counter()
|
|
125
135
|
try:
|
|
126
136
|
result = await call_next(context)
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
context, "request_success"
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
formatted_message = self._format_message(request_success_log_message)
|
|
133
|
-
self.logger.log(self.log_level, f"Completed message: {formatted_message}")
|
|
138
|
+
self._log_message(self._create_after_message(context, start_time))
|
|
134
139
|
|
|
135
140
|
return result
|
|
136
141
|
except Exception as e:
|
|
137
|
-
self.
|
|
138
|
-
|
|
142
|
+
self._log_message(
|
|
143
|
+
self._create_error_message(context, start_time, e), logging.ERROR
|
|
139
144
|
)
|
|
140
145
|
raise
|
|
141
146
|
|
|
@@ -184,7 +189,7 @@ class LoggingMiddleware(BaseLoggingMiddleware):
|
|
|
184
189
|
payload_serializer: Callable that converts objects to a JSON string for the
|
|
185
190
|
payload. If not provided, uses FastMCP's default tool serializer.
|
|
186
191
|
"""
|
|
187
|
-
self.logger: Logger = logger or logging.getLogger("fastmcp.
|
|
192
|
+
self.logger: Logger = logger or logging.getLogger("fastmcp.middleware.logging")
|
|
188
193
|
self.log_level = log_level
|
|
189
194
|
self.include_payloads: bool = include_payloads
|
|
190
195
|
self.include_payload_length: bool = include_payload_length
|
|
@@ -234,7 +239,9 @@ class StructuredLoggingMiddleware(BaseLoggingMiddleware):
|
|
|
234
239
|
payload_serializer: Callable that converts objects to a JSON string for the
|
|
235
240
|
payload. If not provided, uses FastMCP's default tool serializer.
|
|
236
241
|
"""
|
|
237
|
-
self.logger: Logger = logger or logging.getLogger(
|
|
242
|
+
self.logger: Logger = logger or logging.getLogger(
|
|
243
|
+
"fastmcp.middleware.structured_logging"
|
|
244
|
+
)
|
|
238
245
|
self.log_level: int = log_level
|
|
239
246
|
self.include_payloads: bool = include_payloads
|
|
240
247
|
self.include_payload_length: bool = include_payload_length
|
|
@@ -243,3 +250,7 @@ class StructuredLoggingMiddleware(BaseLoggingMiddleware):
|
|
|
243
250
|
self.payload_serializer: Callable[[Any], str] | None = payload_serializer
|
|
244
251
|
self.max_payload_length: int | None = None
|
|
245
252
|
self.structured_logging: bool = True
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _get_duration_ms(start_time: float, /) -> float:
|
|
256
|
+
return round(number=(time.perf_counter() - start_time) * 1000, ndigits=2)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from collections.abc import Awaitable
|
|
4
|
+
from collections.abc import Awaitable, Sequence
|
|
5
5
|
from dataclasses import dataclass, field, replace
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from functools import partial
|
|
@@ -27,9 +27,9 @@ if TYPE_CHECKING:
|
|
|
27
27
|
from fastmcp.server.context import Context
|
|
28
28
|
|
|
29
29
|
__all__ = [
|
|
30
|
+
"CallNext",
|
|
30
31
|
"Middleware",
|
|
31
32
|
"MiddlewareContext",
|
|
32
|
-
"CallNext",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
@@ -99,6 +99,8 @@ class Middleware:
|
|
|
99
99
|
handler = call_next
|
|
100
100
|
|
|
101
101
|
match context.method:
|
|
102
|
+
case "initialize":
|
|
103
|
+
handler = partial(self.on_initialize, call_next=handler)
|
|
102
104
|
case "tools/call":
|
|
103
105
|
handler = partial(self.on_call_tool, call_next=handler)
|
|
104
106
|
case "resources/read":
|
|
@@ -133,18 +135,25 @@ class Middleware:
|
|
|
133
135
|
|
|
134
136
|
async def on_request(
|
|
135
137
|
self,
|
|
136
|
-
context: MiddlewareContext[mt.Request],
|
|
137
|
-
call_next: CallNext[mt.Request, Any],
|
|
138
|
+
context: MiddlewareContext[mt.Request[Any, Any]],
|
|
139
|
+
call_next: CallNext[mt.Request[Any, Any], Any],
|
|
138
140
|
) -> Any:
|
|
139
141
|
return await call_next(context)
|
|
140
142
|
|
|
141
143
|
async def on_notification(
|
|
142
144
|
self,
|
|
143
|
-
context: MiddlewareContext[mt.Notification],
|
|
144
|
-
call_next: CallNext[mt.Notification, Any],
|
|
145
|
+
context: MiddlewareContext[mt.Notification[Any, Any]],
|
|
146
|
+
call_next: CallNext[mt.Notification[Any, Any], Any],
|
|
145
147
|
) -> Any:
|
|
146
148
|
return await call_next(context)
|
|
147
149
|
|
|
150
|
+
async def on_initialize(
|
|
151
|
+
self,
|
|
152
|
+
context: MiddlewareContext[mt.InitializeRequest],
|
|
153
|
+
call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],
|
|
154
|
+
) -> mt.InitializeResult | None:
|
|
155
|
+
return await call_next(context)
|
|
156
|
+
|
|
148
157
|
async def on_call_tool(
|
|
149
158
|
self,
|
|
150
159
|
context: MiddlewareContext[mt.CallToolRequestParams],
|
|
@@ -155,8 +164,10 @@ class Middleware:
|
|
|
155
164
|
async def on_read_resource(
|
|
156
165
|
self,
|
|
157
166
|
context: MiddlewareContext[mt.ReadResourceRequestParams],
|
|
158
|
-
call_next: CallNext[
|
|
159
|
-
|
|
167
|
+
call_next: CallNext[
|
|
168
|
+
mt.ReadResourceRequestParams, Sequence[ReadResourceContents]
|
|
169
|
+
],
|
|
170
|
+
) -> Sequence[ReadResourceContents]:
|
|
160
171
|
return await call_next(context)
|
|
161
172
|
|
|
162
173
|
async def on_get_prompt(
|
|
@@ -169,27 +180,29 @@ class Middleware:
|
|
|
169
180
|
async def on_list_tools(
|
|
170
181
|
self,
|
|
171
182
|
context: MiddlewareContext[mt.ListToolsRequest],
|
|
172
|
-
call_next: CallNext[mt.ListToolsRequest,
|
|
173
|
-
) ->
|
|
183
|
+
call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
|
|
184
|
+
) -> Sequence[Tool]:
|
|
174
185
|
return await call_next(context)
|
|
175
186
|
|
|
176
187
|
async def on_list_resources(
|
|
177
188
|
self,
|
|
178
189
|
context: MiddlewareContext[mt.ListResourcesRequest],
|
|
179
|
-
call_next: CallNext[mt.ListResourcesRequest,
|
|
180
|
-
) ->
|
|
190
|
+
call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]],
|
|
191
|
+
) -> Sequence[Resource]:
|
|
181
192
|
return await call_next(context)
|
|
182
193
|
|
|
183
194
|
async def on_list_resource_templates(
|
|
184
195
|
self,
|
|
185
196
|
context: MiddlewareContext[mt.ListResourceTemplatesRequest],
|
|
186
|
-
call_next: CallNext[
|
|
187
|
-
|
|
197
|
+
call_next: CallNext[
|
|
198
|
+
mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]
|
|
199
|
+
],
|
|
200
|
+
) -> Sequence[ResourceTemplate]:
|
|
188
201
|
return await call_next(context)
|
|
189
202
|
|
|
190
203
|
async def on_list_prompts(
|
|
191
204
|
self,
|
|
192
205
|
context: MiddlewareContext[mt.ListPromptsRequest],
|
|
193
|
-
call_next: CallNext[mt.ListPromptsRequest,
|
|
194
|
-
) ->
|
|
206
|
+
call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]],
|
|
207
|
+
) -> Sequence[Prompt]:
|
|
195
208
|
return await call_next(context)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Rate limiting middleware for protecting FastMCP servers from abuse."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import time
|
|
5
4
|
from collections import defaultdict, deque
|
|
6
5
|
from collections.abc import Callable
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
8
|
+
import anyio
|
|
9
9
|
from mcp import McpError
|
|
10
10
|
from mcp.types import ErrorData
|
|
11
11
|
|
|
@@ -33,7 +33,7 @@ class TokenBucketRateLimiter:
|
|
|
33
33
|
self.refill_rate = refill_rate
|
|
34
34
|
self.tokens = capacity
|
|
35
35
|
self.last_refill = time.time()
|
|
36
|
-
self._lock =
|
|
36
|
+
self._lock = anyio.Lock()
|
|
37
37
|
|
|
38
38
|
async def consume(self, tokens: int = 1) -> bool:
|
|
39
39
|
"""Try to consume tokens from the bucket.
|
|
@@ -71,7 +71,7 @@ class SlidingWindowRateLimiter:
|
|
|
71
71
|
self.max_requests = max_requests
|
|
72
72
|
self.window_seconds = window_seconds
|
|
73
73
|
self.requests = deque()
|
|
74
|
-
self._lock =
|
|
74
|
+
self._lock = anyio.Lock()
|
|
75
75
|
|
|
76
76
|
async def is_allowed(self) -> bool:
|
|
77
77
|
"""Check if a request is allowed."""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""A middleware for injecting tools into the MCP server context."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
import mcp.types
|
|
8
|
+
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
9
|
+
from mcp.types import Prompt
|
|
10
|
+
from pydantic import AnyUrl
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from fastmcp.server.context import Context
|
|
14
|
+
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
|
|
15
|
+
from fastmcp.tools.tool import Tool, ToolResult
|
|
16
|
+
from fastmcp.utilities.logging import get_logger
|
|
17
|
+
|
|
18
|
+
logger: Logger = get_logger(name=__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolInjectionMiddleware(Middleware):
|
|
22
|
+
"""A middleware for injecting tools into the context."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, tools: Sequence[Tool]):
|
|
25
|
+
"""Initialize the tool injection middleware."""
|
|
26
|
+
self._tools_to_inject: Sequence[Tool] = tools
|
|
27
|
+
self._tools_to_inject_by_name: dict[str, Tool] = {
|
|
28
|
+
tool.name: tool for tool in tools
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
async def on_list_tools(
|
|
33
|
+
self,
|
|
34
|
+
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
|
35
|
+
call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]],
|
|
36
|
+
) -> Sequence[Tool]:
|
|
37
|
+
"""Inject tools into the response."""
|
|
38
|
+
return [*self._tools_to_inject, *await call_next(context)]
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
async def on_call_tool(
|
|
42
|
+
self,
|
|
43
|
+
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
|
44
|
+
call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult],
|
|
45
|
+
) -> ToolResult:
|
|
46
|
+
"""Intercept tool calls to injected tools."""
|
|
47
|
+
if context.message.name in self._tools_to_inject_by_name:
|
|
48
|
+
tool = self._tools_to_inject_by_name[context.message.name]
|
|
49
|
+
return await tool.run(arguments=context.message.arguments or {})
|
|
50
|
+
|
|
51
|
+
return await call_next(context)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def list_prompts(context: Context) -> list[Prompt]:
|
|
55
|
+
"""List prompts available on the server."""
|
|
56
|
+
return await context.list_prompts()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
list_prompts_tool = Tool.from_function(
|
|
60
|
+
fn=list_prompts,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def get_prompt(
|
|
65
|
+
context: Context,
|
|
66
|
+
name: Annotated[str, "The name of the prompt to render."],
|
|
67
|
+
arguments: Annotated[
|
|
68
|
+
dict[str, Any] | None, "The arguments to pass to the prompt."
|
|
69
|
+
] = None,
|
|
70
|
+
) -> mcp.types.GetPromptResult:
|
|
71
|
+
"""Render a prompt available on the server."""
|
|
72
|
+
return await context.get_prompt(name=name, arguments=arguments)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
get_prompt_tool = Tool.from_function(
|
|
76
|
+
fn=get_prompt,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PromptToolMiddleware(ToolInjectionMiddleware):
|
|
81
|
+
"""A middleware for injecting prompts as tools into the context."""
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
tools: list[Tool] = [list_prompts_tool, get_prompt_tool]
|
|
85
|
+
super().__init__(tools=tools)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def list_resources(context: Context) -> list[mcp.types.Resource]:
|
|
89
|
+
"""List resources available on the server."""
|
|
90
|
+
return await context.list_resources()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
list_resources_tool = Tool.from_function(
|
|
94
|
+
fn=list_resources,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def read_resource(
|
|
99
|
+
context: Context,
|
|
100
|
+
uri: Annotated[AnyUrl | str, "The URI of the resource to read."],
|
|
101
|
+
) -> list[ReadResourceContents]:
|
|
102
|
+
"""Read a resource available on the server."""
|
|
103
|
+
return await context.read_resource(uri=uri)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
read_resource_tool = Tool.from_function(
|
|
107
|
+
fn=read_resource,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ResourceToolMiddleware(ToolInjectionMiddleware):
|
|
112
|
+
"""A middleware for injecting resources as tools into the context."""
|
|
113
|
+
|
|
114
|
+
def __init__(self) -> None:
|
|
115
|
+
tools: list[Tool] = [list_resources_tool, read_resource_tool]
|
|
116
|
+
super().__init__(tools=tools)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""OpenAPI server implementation for FastMCP - refactored for better maintainability."""
|
|
2
|
+
|
|
3
|
+
# Import from server
|
|
4
|
+
from .server import FastMCPOpenAPI
|
|
5
|
+
|
|
6
|
+
# Import from routing
|
|
7
|
+
from .routing import (
|
|
8
|
+
MCPType,
|
|
9
|
+
RouteMap,
|
|
10
|
+
RouteMapFn,
|
|
11
|
+
ComponentFn,
|
|
12
|
+
DEFAULT_ROUTE_MAPPINGS,
|
|
13
|
+
_determine_route_type,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Import from components
|
|
17
|
+
from .components import (
|
|
18
|
+
OpenAPITool,
|
|
19
|
+
OpenAPIResource,
|
|
20
|
+
OpenAPIResourceTemplate,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Export public symbols - maintaining backward compatibility
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DEFAULT_ROUTE_MAPPINGS",
|
|
26
|
+
"ComponentFn",
|
|
27
|
+
"FastMCPOpenAPI",
|
|
28
|
+
"MCPType",
|
|
29
|
+
"OpenAPIResource",
|
|
30
|
+
"OpenAPIResourceTemplate",
|
|
31
|
+
"OpenAPITool",
|
|
32
|
+
"RouteMap",
|
|
33
|
+
"RouteMapFn",
|
|
34
|
+
"_determine_route_type",
|
|
35
|
+
]
|
|
@@ -9,14 +9,15 @@ import httpx
|
|
|
9
9
|
from mcp.types import ToolAnnotations
|
|
10
10
|
from pydantic.networks import AnyUrl
|
|
11
11
|
|
|
12
|
-
# Import from our new utilities
|
|
13
|
-
from fastmcp.experimental.utilities.openapi import HTTPRoute
|
|
14
|
-
from fastmcp.experimental.utilities.openapi.director import RequestDirector
|
|
15
12
|
from fastmcp.resources import Resource, ResourceTemplate
|
|
16
13
|
from fastmcp.server.dependencies import get_http_headers
|
|
17
14
|
from fastmcp.tools.tool import Tool, ToolResult
|
|
18
15
|
from fastmcp.utilities.logging import get_logger
|
|
19
16
|
|
|
17
|
+
# Import from our new utilities
|
|
18
|
+
from fastmcp.utilities.openapi import HTTPRoute
|
|
19
|
+
from fastmcp.utilities.openapi.director import RequestDirector
|
|
20
|
+
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
22
|
from fastmcp.server import Context
|
|
22
23
|
|
|
@@ -146,11 +147,11 @@ class OpenAPITool(Tool):
|
|
|
146
147
|
if e.response.text:
|
|
147
148
|
error_message += f" - {e.response.text}"
|
|
148
149
|
|
|
149
|
-
raise ValueError(error_message)
|
|
150
|
+
raise ValueError(error_message) from e
|
|
150
151
|
|
|
151
152
|
except httpx.RequestError as e:
|
|
152
153
|
# Handle request errors (connection, timeout, etc.)
|
|
153
|
-
raise ValueError(f"Request error: {
|
|
154
|
+
raise ValueError(f"Request error: {e!s}") from e
|
|
154
155
|
|
|
155
156
|
|
|
156
157
|
class OpenAPIResource(Resource):
|
|
@@ -165,9 +166,11 @@ class OpenAPIResource(Resource):
|
|
|
165
166
|
name: str,
|
|
166
167
|
description: str,
|
|
167
168
|
mime_type: str = "application/json",
|
|
168
|
-
tags: set[str] =
|
|
169
|
+
tags: set[str] | None = None,
|
|
169
170
|
timeout: float | None = None,
|
|
170
171
|
):
|
|
172
|
+
if tags is None:
|
|
173
|
+
tags = set()
|
|
171
174
|
super().__init__(
|
|
172
175
|
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
173
176
|
name=name,
|
|
@@ -276,11 +279,11 @@ class OpenAPIResource(Resource):
|
|
|
276
279
|
if e.response.text:
|
|
277
280
|
error_message += f" - {e.response.text}"
|
|
278
281
|
|
|
279
|
-
raise ValueError(error_message)
|
|
282
|
+
raise ValueError(error_message) from e
|
|
280
283
|
|
|
281
284
|
except httpx.RequestError as e:
|
|
282
285
|
# Handle request errors (connection, timeout, etc.)
|
|
283
|
-
raise ValueError(f"Request error: {
|
|
286
|
+
raise ValueError(f"Request error: {e!s}") from e
|
|
284
287
|
|
|
285
288
|
|
|
286
289
|
class OpenAPIResourceTemplate(ResourceTemplate):
|
|
@@ -295,9 +298,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
295
298
|
name: str,
|
|
296
299
|
description: str,
|
|
297
300
|
parameters: dict[str, Any],
|
|
298
|
-
tags: set[str] =
|
|
301
|
+
tags: set[str] | None = None,
|
|
299
302
|
timeout: float | None = None,
|
|
300
303
|
):
|
|
304
|
+
if tags is None:
|
|
305
|
+
tags = set()
|
|
301
306
|
super().__init__(
|
|
302
307
|
uri_template=uri_template,
|
|
303
308
|
name=name,
|
|
@@ -342,7 +347,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
342
347
|
|
|
343
348
|
# Export public symbols
|
|
344
349
|
__all__ = [
|
|
345
|
-
"OpenAPITool",
|
|
346
350
|
"OpenAPIResource",
|
|
347
351
|
"OpenAPIResourceTemplate",
|
|
352
|
+
"OpenAPITool",
|
|
348
353
|
]
|
|
@@ -14,8 +14,8 @@ if TYPE_CHECKING:
|
|
|
14
14
|
OpenAPITool,
|
|
15
15
|
)
|
|
16
16
|
# Import from our new utilities
|
|
17
|
-
from fastmcp.experimental.utilities.openapi import HttpMethod, HTTPRoute
|
|
18
17
|
from fastmcp.utilities.logging import get_logger
|
|
18
|
+
from fastmcp.utilities.openapi import HttpMethod, HTTPRoute
|
|
19
19
|
|
|
20
20
|
logger = get_logger(__name__)
|
|
21
21
|
|
|
@@ -121,10 +121,10 @@ def _determine_route_type(
|
|
|
121
121
|
|
|
122
122
|
# Export public symbols
|
|
123
123
|
__all__ = [
|
|
124
|
+
"DEFAULT_ROUTE_MAPPINGS",
|
|
125
|
+
"ComponentFn",
|
|
124
126
|
"MCPType",
|
|
125
127
|
"RouteMap",
|
|
126
128
|
"RouteMapFn",
|
|
127
|
-
"ComponentFn",
|
|
128
|
-
"DEFAULT_ROUTE_MAPPINGS",
|
|
129
129
|
"_determine_route_type",
|
|
130
130
|
]
|
|
@@ -7,16 +7,17 @@ from typing import Any, Literal
|
|
|
7
7
|
import httpx
|
|
8
8
|
from jsonschema_path import SchemaPath
|
|
9
9
|
|
|
10
|
+
from fastmcp.server.server import FastMCP
|
|
11
|
+
from fastmcp.utilities.logging import get_logger
|
|
12
|
+
|
|
10
13
|
# Import from our new utilities and components
|
|
11
|
-
from fastmcp.
|
|
14
|
+
from fastmcp.utilities.openapi import (
|
|
12
15
|
HTTPRoute,
|
|
13
16
|
extract_output_schema_from_responses,
|
|
14
17
|
format_simple_description,
|
|
15
18
|
parse_openapi_to_http_routes,
|
|
16
19
|
)
|
|
17
|
-
from fastmcp.
|
|
18
|
-
from fastmcp.server.server import FastMCP
|
|
19
|
-
from fastmcp.utilities.logging import get_logger
|
|
20
|
+
from fastmcp.utilities.openapi.director import RequestDirector
|
|
20
21
|
|
|
21
22
|
from .components import (
|
|
22
23
|
OpenAPIResource,
|
|
@@ -247,7 +248,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
|
247
248
|
# Create the new name
|
|
248
249
|
new_name = f"{name}_{self._used_names[component_type][name]}"
|
|
249
250
|
logger.debug(
|
|
250
|
-
f"Name collision detected: '{name}' already exists as a {component_type
|
|
251
|
+
f"Name collision detected: '{name}' already exists as a {component_type}. "
|
|
251
252
|
f"Using '{new_name}' instead."
|
|
252
253
|
)
|
|
253
254
|
|