fastmcp 2.12.5__py3-none-any.whl → 2.13.2__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 -2
- fastmcp/cli/cli.py +11 -11
- 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/run.py +13 -8
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +115 -217
- fastmcp/client/client.py +105 -39
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +80 -25
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +6 -6
- 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/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +14 -15
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +22 -19
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +14 -9
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +70 -43
- 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 +1510 -289
- fastmcp/server/auth/oidc_proxy.py +84 -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/bearer.py +1 -1
- 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 +27 -3
- 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 +177 -51
- fastmcp/server/dependencies.py +39 -12
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +56 -17
- fastmcp/server/low_level.py +121 -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.py +10 -6
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +725 -242
- fastmcp/settings.py +24 -10
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +70 -23
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -10
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +118 -8
- 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 +4 -4
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +85 -4
- fastmcp/utilities/types.py +78 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,9 +6,12 @@ import traceback
|
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
import anyio
|
|
9
10
|
from mcp import McpError
|
|
10
11
|
from mcp.types import ErrorData
|
|
11
12
|
|
|
13
|
+
from fastmcp.exceptions import NotFoundError
|
|
14
|
+
|
|
12
15
|
from .middleware import CallNext, Middleware, MiddlewareContext
|
|
13
16
|
|
|
14
17
|
|
|
@@ -61,7 +64,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
61
64
|
error_key = f"{error_type}:{method}"
|
|
62
65
|
self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1
|
|
63
66
|
|
|
64
|
-
base_message = f"Error in {method}: {error_type}: {
|
|
67
|
+
base_message = f"Error in {method}: {error_type}: {error!s}"
|
|
65
68
|
|
|
66
69
|
if self.include_traceback:
|
|
67
70
|
self.logger.error(f"{base_message}\n{traceback.format_exc()}")
|
|
@@ -88,23 +91,24 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
88
91
|
|
|
89
92
|
if error_type in (ValueError, TypeError):
|
|
90
93
|
return McpError(
|
|
91
|
-
ErrorData(code=-32602, message=f"Invalid params: {
|
|
94
|
+
ErrorData(code=-32602, message=f"Invalid params: {error!s}")
|
|
92
95
|
)
|
|
93
|
-
elif error_type in (FileNotFoundError, KeyError):
|
|
96
|
+
elif error_type in (FileNotFoundError, KeyError, NotFoundError):
|
|
94
97
|
return McpError(
|
|
95
|
-
ErrorData(code=-32001, message=f"Resource not found: {
|
|
98
|
+
ErrorData(code=-32001, message=f"Resource not found: {error!s}")
|
|
96
99
|
)
|
|
97
100
|
elif error_type is PermissionError:
|
|
98
101
|
return McpError(
|
|
99
|
-
ErrorData(code=-32000, message=f"Permission denied: {
|
|
102
|
+
ErrorData(code=-32000, message=f"Permission denied: {error!s}")
|
|
100
103
|
)
|
|
104
|
+
# asyncio.TimeoutError is a subclass of TimeoutError in Python 3.10, alias in 3.11+
|
|
101
105
|
elif error_type in (TimeoutError, asyncio.TimeoutError):
|
|
102
106
|
return McpError(
|
|
103
|
-
ErrorData(code=-32000, message=f"Request timeout: {
|
|
107
|
+
ErrorData(code=-32000, message=f"Request timeout: {error!s}")
|
|
104
108
|
)
|
|
105
109
|
else:
|
|
106
110
|
return McpError(
|
|
107
|
-
ErrorData(code=-32603, message=f"Internal error: {
|
|
111
|
+
ErrorData(code=-32603, message=f"Internal error: {error!s}")
|
|
108
112
|
)
|
|
109
113
|
|
|
110
114
|
async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
|
|
@@ -116,7 +120,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|
|
116
120
|
|
|
117
121
|
# Transform and re-raise
|
|
118
122
|
transformed_error = self._transform_error(error)
|
|
119
|
-
raise transformed_error
|
|
123
|
+
raise transformed_error from error
|
|
120
124
|
|
|
121
125
|
def get_error_stats(self) -> dict[str, int]:
|
|
122
126
|
"""Get error statistics for monitoring."""
|
|
@@ -196,10 +200,10 @@ class RetryMiddleware(Middleware):
|
|
|
196
200
|
delay = self._calculate_delay(attempt)
|
|
197
201
|
self.logger.warning(
|
|
198
202
|
f"Request {context.method} failed (attempt {attempt + 1}/{self.max_retries + 1}): "
|
|
199
|
-
f"{type(error).__name__}: {
|
|
203
|
+
f"{type(error).__name__}: {error!s}. Retrying in {delay:.1f}s..."
|
|
200
204
|
)
|
|
201
205
|
|
|
202
|
-
await
|
|
206
|
+
await anyio.sleep(delay)
|
|
203
207
|
|
|
204
208
|
# Re-raise the last error if all retries failed
|
|
205
209
|
if last_error:
|
|
@@ -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, None],
|
|
154
|
+
) -> 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)
|
fastmcp/server/openapi.py
CHANGED
|
@@ -513,11 +513,11 @@ class OpenAPITool(Tool):
|
|
|
513
513
|
if e.response.text:
|
|
514
514
|
error_message += f" - {e.response.text}"
|
|
515
515
|
|
|
516
|
-
raise ValueError(error_message)
|
|
516
|
+
raise ValueError(error_message) from e
|
|
517
517
|
|
|
518
518
|
except httpx.RequestError as e:
|
|
519
519
|
# Handle request errors (connection, timeout, etc.)
|
|
520
|
-
raise ValueError(f"Request error: {
|
|
520
|
+
raise ValueError(f"Request error: {e!s}") from e
|
|
521
521
|
|
|
522
522
|
|
|
523
523
|
class OpenAPIResource(Resource):
|
|
@@ -531,9 +531,11 @@ class OpenAPIResource(Resource):
|
|
|
531
531
|
name: str,
|
|
532
532
|
description: str,
|
|
533
533
|
mime_type: str = "application/json",
|
|
534
|
-
tags: set[str] =
|
|
534
|
+
tags: set[str] | None = None,
|
|
535
535
|
timeout: float | None = None,
|
|
536
536
|
):
|
|
537
|
+
if tags is None:
|
|
538
|
+
tags = set()
|
|
537
539
|
super().__init__(
|
|
538
540
|
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
539
541
|
name=name,
|
|
@@ -632,11 +634,11 @@ class OpenAPIResource(Resource):
|
|
|
632
634
|
if e.response.text:
|
|
633
635
|
error_message += f" - {e.response.text}"
|
|
634
636
|
|
|
635
|
-
raise ValueError(error_message)
|
|
637
|
+
raise ValueError(error_message) from e
|
|
636
638
|
|
|
637
639
|
except httpx.RequestError as e:
|
|
638
640
|
# Handle request errors (connection, timeout, etc.)
|
|
639
|
-
raise ValueError(f"Request error: {
|
|
641
|
+
raise ValueError(f"Request error: {e!s}") from e
|
|
640
642
|
|
|
641
643
|
|
|
642
644
|
class OpenAPIResourceTemplate(ResourceTemplate):
|
|
@@ -650,9 +652,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
650
652
|
name: str,
|
|
651
653
|
description: str,
|
|
652
654
|
parameters: dict[str, Any],
|
|
653
|
-
tags: set[str] =
|
|
655
|
+
tags: set[str] | None = None,
|
|
654
656
|
timeout: float | None = None,
|
|
655
657
|
):
|
|
658
|
+
if tags is None:
|
|
659
|
+
tags = set()
|
|
656
660
|
super().__init__(
|
|
657
661
|
uri_template=uri_template,
|
|
658
662
|
name=name,
|