fastmcp 2.12.4__py3-none-any.whl → 2.13.0rc1__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.
Files changed (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +81 -171
  12. fastmcp/client/transports.py +76 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1238 -234
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +12 -6
  28. fastmcp/server/auth/providers/aws.py +13 -2
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +13 -7
  32. fastmcp/server/auth/providers/google.py +13 -7
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +16 -13
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +53 -16
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +2 -2
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.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], event: str
57
+ self, context: MiddlewareContext[Any]
61
58
  ) -> dict[str, str | int]:
62
- message = self._create_base_message(context, event)
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 _create_after_message(
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
- event: str,
97
- ) -> dict[str, str | int]:
98
- """Format a message for logging."""
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
- parts: dict[str, str | int] = {
101
- "event": event,
102
- "timestamp": self._get_timestamp_from_context(context),
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
- return parts
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 all messages."""
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
- request_start_log_message = self._create_before_message(
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
- request_success_log_message = self._create_after_message(
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.logger.log(
138
- logging.ERROR, f"Failed message: {context.method or 'unknown'} - {e}"
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.requests")
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("fastmcp.structured")
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[mt.ReadResourceRequestParams, list[ReadResourceContents]],
159
- ) -> list[ReadResourceContents]:
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, list[Tool]],
173
- ) -> list[Tool]:
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, list[Resource]],
180
- ) -> list[Resource]:
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[mt.ListResourceTemplatesRequest, list[ResourceTemplate]],
187
- ) -> list[ResourceTemplate]:
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, list[Prompt]],
194
- ) -> list[Prompt]:
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 = asyncio.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 = asyncio.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