fastmcp 2.12.5__py3-none-any.whl → 2.13.0rc2__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/cli/cli.py +6 -6
- fastmcp/cli/install/claude_code.py +3 -3
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +7 -7
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/auth/oauth.py +100 -208
- fastmcp/client/client.py +11 -11
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +81 -171
- fastmcp/client/transports.py +76 -22
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
- fastmcp/experimental/utilities/openapi/parser.py +23 -3
- fastmcp/prompts/prompt.py +13 -6
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/resource.py +13 -6
- fastmcp/resources/resource_manager.py +5 -164
- fastmcp/resources/template.py +107 -17
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/jwt_issuer.py +289 -0
- fastmcp/server/auth/oauth_proxy.py +1228 -233
- fastmcp/server/auth/oidc_proxy.py +8 -6
- fastmcp/server/auth/providers/auth0.py +13 -7
- fastmcp/server/auth/providers/aws.py +14 -3
- fastmcp/server/auth/providers/azure.py +137 -124
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +14 -8
- fastmcp/server/auth/providers/google.py +15 -9
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +8 -2
- fastmcp/server/auth/providers/scalekit.py +179 -0
- fastmcp/server/auth/providers/supabase.py +172 -0
- fastmcp/server/auth/providers/workos.py +17 -14
- fastmcp/server/context.py +89 -34
- fastmcp/server/http.py +57 -17
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/caching.py +469 -0
- fastmcp/server/middleware/error_handling.py +6 -2
- fastmcp/server/middleware/logging.py +48 -37
- fastmcp/server/middleware/middleware.py +28 -15
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +638 -183
- fastmcp/settings.py +22 -9
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +22 -108
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +32 -22
- fastmcp/utilities/components.py +5 -0
- fastmcp/utilities/inspect.py +77 -21
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/tests.py +87 -4
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/ui.py +497 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/METADATA +8 -4
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/RECORD +66 -62
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.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
|
|
@@ -52,14 +53,14 @@ class BaseLoggingMiddleware(Middleware):
|
|
|
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]
|
|
57
|
+
self, context: MiddlewareContext[Any]
|
|
61
58
|
) -> dict[str, str | int]:
|
|
62
|
-
message =
|
|
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
|
|
@@ -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.InitializeRequestParams],
|
|
153
|
+
call_next: CallNext[mt.InitializeRequestParams, 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."""
|
fastmcp/server/proxy.py
CHANGED
|
@@ -69,7 +69,7 @@ class ProxyManagerMixin:
|
|
|
69
69
|
class ProxyToolManager(ToolManager, ProxyManagerMixin):
|
|
70
70
|
"""A ToolManager that sources its tools from a remote client in addition to local and mounted tools."""
|
|
71
71
|
|
|
72
|
-
def __init__(self, client_factory: ClientFactoryT, **kwargs):
|
|
72
|
+
def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
|
|
73
73
|
super().__init__(**kwargs)
|
|
74
74
|
self.client_factory = client_factory
|
|
75
75
|
|
|
@@ -123,7 +123,7 @@ class ProxyToolManager(ToolManager, ProxyManagerMixin):
|
|
|
123
123
|
class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
|
|
124
124
|
"""A ResourceManager that sources its resources from a remote client in addition to local and mounted resources."""
|
|
125
125
|
|
|
126
|
-
def __init__(self, client_factory: ClientFactoryT, **kwargs):
|
|
126
|
+
def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
|
|
127
127
|
super().__init__(**kwargs)
|
|
128
128
|
self.client_factory = client_factory
|
|
129
129
|
|
|
@@ -204,7 +204,7 @@ class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
|
|
|
204
204
|
class ProxyPromptManager(PromptManager, ProxyManagerMixin):
|
|
205
205
|
"""A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts."""
|
|
206
206
|
|
|
207
|
-
def __init__(self, client_factory: ClientFactoryT, **kwargs):
|
|
207
|
+
def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
|
|
208
208
|
super().__init__(**kwargs)
|
|
209
209
|
self.client_factory = client_factory
|
|
210
210
|
|
|
@@ -258,7 +258,7 @@ class ProxyTool(Tool, MirroredComponent):
|
|
|
258
258
|
A Tool that represents and executes a tool on a remote server.
|
|
259
259
|
"""
|
|
260
260
|
|
|
261
|
-
def __init__(self, client: Client, **kwargs):
|
|
261
|
+
def __init__(self, client: Client, **kwargs: Any):
|
|
262
262
|
super().__init__(**kwargs)
|
|
263
263
|
self._client = client
|
|
264
264
|
|
|
@@ -354,7 +354,7 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent):
|
|
|
354
354
|
A ResourceTemplate that represents and creates resources from a remote server template.
|
|
355
355
|
"""
|
|
356
356
|
|
|
357
|
-
def __init__(self, client: Client, **kwargs):
|
|
357
|
+
def __init__(self, client: Client, **kwargs: Any):
|
|
358
358
|
super().__init__(**kwargs)
|
|
359
359
|
self._client = client
|
|
360
360
|
|
|
@@ -640,7 +640,7 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
|
|
|
640
640
|
Note that it is essential to ensure that the proxy server itself is also stateful.
|
|
641
641
|
"""
|
|
642
642
|
|
|
643
|
-
def __init__(self, *args, **kwargs):
|
|
643
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
|
644
644
|
super().__init__(*args, **kwargs)
|
|
645
645
|
self._caches: dict[ServerSession, Client[ClientTransportT]] = {}
|
|
646
646
|
|