fastmcp 2.12.1__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.
Files changed (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/http.py CHANGED
@@ -5,10 +5,12 @@ from contextlib import asynccontextmanager, contextmanager
5
5
  from contextvars import ContextVar
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware
8
+ from mcp.server.auth.routes import build_resource_metadata_url
9
9
  from mcp.server.lowlevel.server import LifespanResultT
10
10
  from mcp.server.sse import SseServerTransport
11
- from mcp.server.streamable_http import EventStore
11
+ from mcp.server.streamable_http import (
12
+ EventStore,
13
+ )
12
14
  from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
13
15
  from starlette.applications import Starlette
14
16
  from starlette.middleware import Middleware
@@ -18,6 +20,7 @@ from starlette.routing import BaseRoute, Mount, Route
18
20
  from starlette.types import Lifespan, Receive, Scope, Send
19
21
 
20
22
  from fastmcp.server.auth import AuthProvider
23
+ from fastmcp.server.auth.middleware import RequireAuthMiddleware
21
24
  from fastmcp.utilities.logging import get_logger
22
25
 
23
26
  if TYPE_CHECKING:
@@ -113,7 +116,7 @@ def create_base_app(
113
116
  A Starlette application
114
117
  """
115
118
  # Always add RequestContextMiddleware as the outermost middleware
116
- middleware.append(Middleware(RequestContextMiddleware))
119
+ middleware.insert(0, Middleware(RequestContextMiddleware))
117
120
 
118
121
  return StarletteWithLifespan(
119
122
  routes=routes,
@@ -167,23 +170,38 @@ def create_sse_app(
167
170
  # Get auth middleware from the provider
168
171
  auth_middleware = auth.get_middleware()
169
172
 
170
- # Get auth routes including protected MCP endpoint
171
- auth_routes = auth.get_routes(
172
- mcp_path=sse_path,
173
- mcp_endpoint=handle_sse,
174
- )
175
-
173
+ # Get auth provider's own routes (OAuth endpoints, metadata, etc)
174
+ auth_routes = auth.get_routes(mcp_path=sse_path)
176
175
  server_routes.extend(auth_routes)
177
176
  server_middleware.extend(auth_middleware)
178
177
 
179
- # Manually wrap the SSE message endpoint with RequireAuthMiddleware
178
+ # Build RFC 9728-compliant metadata URL
179
+ resource_url = auth._get_resource_url(sse_path)
180
+ resource_metadata_url = (
181
+ build_resource_metadata_url(resource_url) if resource_url else None
182
+ )
183
+
184
+ # Create protected SSE endpoint route
185
+ server_routes.append(
186
+ Route(
187
+ sse_path,
188
+ endpoint=RequireAuthMiddleware(
189
+ handle_sse,
190
+ auth.required_scopes,
191
+ resource_metadata_url,
192
+ ),
193
+ methods=["GET"],
194
+ )
195
+ )
196
+
197
+ # Wrap the SSE message endpoint with RequireAuthMiddleware
180
198
  server_routes.append(
181
199
  Mount(
182
200
  message_path,
183
201
  app=RequireAuthMiddleware(
184
202
  sse.handle_post_message,
185
203
  auth.required_scopes,
186
- auth._get_resource_url("/.well-known/oauth-protected-resource"),
204
+ resource_metadata_url,
187
205
  ),
188
206
  )
189
207
  )
@@ -215,11 +233,17 @@ def create_sse_app(
215
233
  if middleware:
216
234
  server_middleware.extend(middleware)
217
235
 
236
+ @asynccontextmanager
237
+ async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
238
+ async with server._lifespan_manager():
239
+ yield
240
+
218
241
  # Create and return the app
219
242
  app = create_base_app(
220
243
  routes=server_routes,
221
244
  middleware=server_middleware,
222
245
  debug=debug,
246
+ lifespan=lifespan,
223
247
  )
224
248
  # Store the FastMCP server instance on the Starlette app state
225
249
  app.state.fastmcp_server = server
@@ -274,14 +298,29 @@ def create_streamable_http_app(
274
298
  # Get auth middleware from the provider
275
299
  auth_middleware = auth.get_middleware()
276
300
 
277
- # Get auth routes including protected MCP endpoint
278
- auth_routes = auth.get_routes(
279
- mcp_path=streamable_http_path,
280
- mcp_endpoint=streamable_http_app,
281
- )
282
-
301
+ # Get auth provider's own routes (OAuth endpoints, metadata, etc)
302
+ auth_routes = auth.get_routes(mcp_path=streamable_http_path)
283
303
  server_routes.extend(auth_routes)
284
304
  server_middleware.extend(auth_middleware)
305
+
306
+ # Build RFC 9728-compliant metadata URL
307
+ resource_url = auth._get_resource_url(streamable_http_path)
308
+ resource_metadata_url = (
309
+ build_resource_metadata_url(resource_url) if resource_url else None
310
+ )
311
+
312
+ # Create protected HTTP endpoint route
313
+ server_routes.append(
314
+ Route(
315
+ streamable_http_path,
316
+ endpoint=RequireAuthMiddleware(
317
+ streamable_http_app,
318
+ auth.required_scopes,
319
+ resource_metadata_url,
320
+ ),
321
+ methods=["GET", "POST", "DELETE"],
322
+ )
323
+ )
285
324
  else:
286
325
  # No auth required
287
326
  server_routes.append(
@@ -303,7 +342,7 @@ def create_streamable_http_app(
303
342
  # Create a lifespan manager to start and stop the session manager
304
343
  @asynccontextmanager
305
344
  async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
306
- async with session_manager.run():
345
+ async with server._lifespan_manager(), session_manager.run():
307
346
  yield
308
347
 
309
348
  # Create and return the app with lifespan
@@ -1,5 +1,12 @@
1
- from typing import Any
1
+ from __future__ import annotations
2
2
 
3
+ import weakref
4
+ from contextlib import AsyncExitStack
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import anyio
8
+ import mcp.types
9
+ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
3
10
  from mcp.server.lowlevel.server import (
4
11
  LifespanResultT,
5
12
  NotificationOptions,
@@ -9,11 +16,82 @@ from mcp.server.lowlevel.server import (
9
16
  Server as _Server,
10
17
  )
11
18
  from mcp.server.models import InitializationOptions
19
+ from mcp.server.session import ServerSession
20
+ from mcp.server.stdio import stdio_server as stdio_server
21
+ from mcp.shared.message import SessionMessage
22
+ from mcp.shared.session import RequestResponder
23
+
24
+ from fastmcp.utilities.logging import get_logger
25
+
26
+ if TYPE_CHECKING:
27
+ from fastmcp.server.server import FastMCP
28
+
29
+ logger = get_logger(__name__)
30
+
31
+
32
+ class MiddlewareServerSession(ServerSession):
33
+ """ServerSession that routes initialization requests through FastMCP middleware."""
34
+
35
+ def __init__(self, fastmcp: FastMCP, *args, **kwargs):
36
+ super().__init__(*args, **kwargs)
37
+ self._fastmcp_ref: weakref.ref[FastMCP] = weakref.ref(fastmcp)
38
+
39
+ @property
40
+ def fastmcp(self) -> FastMCP:
41
+ """Get the FastMCP instance."""
42
+ fastmcp = self._fastmcp_ref()
43
+ if fastmcp is None:
44
+ raise RuntimeError("FastMCP instance is no longer available")
45
+ return fastmcp
46
+
47
+ async def _received_request(
48
+ self,
49
+ responder: RequestResponder[mcp.types.ClientRequest, mcp.types.ServerResult],
50
+ ):
51
+ """
52
+ Override the _received_request method to route initialization requests
53
+ through FastMCP middleware.
54
+
55
+ These are not handled by routes that FastMCP typically overrides and
56
+ require special handling.
57
+ """
58
+ import fastmcp.server.context
59
+ from fastmcp.server.middleware.middleware import MiddlewareContext
60
+
61
+ if isinstance(responder.request.root, mcp.types.InitializeRequest):
62
+
63
+ async def call_original_handler(
64
+ ctx: MiddlewareContext,
65
+ ) -> None:
66
+ return await super(MiddlewareServerSession, self)._received_request(
67
+ responder
68
+ )
69
+
70
+ async with fastmcp.server.context.Context(
71
+ fastmcp=self.fastmcp
72
+ ) as fastmcp_ctx:
73
+ # Create the middleware context.
74
+ mw_context = MiddlewareContext(
75
+ message=responder.request.root,
76
+ source="client",
77
+ type="request",
78
+ method="initialize",
79
+ fastmcp_context=fastmcp_ctx,
80
+ )
81
+
82
+ return await self.fastmcp._apply_middleware(
83
+ mw_context, call_original_handler
84
+ )
85
+ else:
86
+ return await super()._received_request(responder)
12
87
 
13
88
 
14
89
  class LowLevelServer(_Server[LifespanResultT, RequestT]):
15
- def __init__(self, *args, **kwargs):
90
+ def __init__(self, fastmcp: FastMCP, *args: Any, **kwargs: Any):
16
91
  super().__init__(*args, **kwargs)
92
+ # Store a weak reference to FastMCP to avoid circular references
93
+ self._fastmcp_ref: weakref.ref[FastMCP] = weakref.ref(fastmcp)
94
+
17
95
  # FastMCP servers support notifications for all components
18
96
  self.notification_options = NotificationOptions(
19
97
  prompts_changed=True,
@@ -21,6 +99,14 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
21
99
  tools_changed=True,
22
100
  )
23
101
 
102
+ @property
103
+ def fastmcp(self) -> FastMCP:
104
+ """Get the FastMCP instance."""
105
+ fastmcp = self._fastmcp_ref()
106
+ if fastmcp is None:
107
+ raise RuntimeError("FastMCP instance is no longer available")
108
+ return fastmcp
109
+
24
110
  def create_initialization_options(
25
111
  self,
26
112
  notification_options: NotificationOptions | None = None,
@@ -35,3 +121,36 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
35
121
  experimental_capabilities=experimental_capabilities,
36
122
  **kwargs,
37
123
  )
124
+
125
+ async def run(
126
+ self,
127
+ read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
128
+ write_stream: MemoryObjectSendStream[SessionMessage],
129
+ initialization_options: InitializationOptions,
130
+ raise_exceptions: bool = False,
131
+ stateless: bool = False,
132
+ ):
133
+ """
134
+ Overrides the run method to use the MiddlewareServerSession.
135
+ """
136
+ async with AsyncExitStack() as stack:
137
+ lifespan_context = await stack.enter_async_context(self.lifespan(self))
138
+ session = await stack.enter_async_context(
139
+ MiddlewareServerSession(
140
+ self.fastmcp,
141
+ read_stream,
142
+ write_stream,
143
+ initialization_options,
144
+ stateless=stateless,
145
+ )
146
+ )
147
+
148
+ async with anyio.create_task_group() as tg:
149
+ async for message in session.incoming_messages:
150
+ tg.start_soon(
151
+ self._handle_message,
152
+ message,
153
+ session,
154
+ lifespan_context,
155
+ raise_exceptions,
156
+ )
@@ -5,7 +5,7 @@ from .middleware import (
5
5
  )
6
6
 
7
7
  __all__ = [
8
+ "CallNext",
8
9
  "Middleware",
9
10
  "MiddlewareContext",
10
- "CallNext",
11
11
  ]