fastmcp 2.2.9__py3-none-any.whl → 2.3.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.
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, ParamSpec, TypeVar
4
+
5
+ from starlette.requests import Request
6
+
7
+ if TYPE_CHECKING:
8
+ from fastmcp.server.context import Context
9
+
10
+ P = ParamSpec("P")
11
+ R = TypeVar("R")
12
+
13
+
14
+ # --- Context ---
15
+
16
+
17
+ def get_context() -> Context:
18
+ from fastmcp.server.context import _current_context
19
+
20
+ context = _current_context.get()
21
+ if context is None:
22
+ raise RuntimeError("No active context found.")
23
+ return context
24
+
25
+
26
+ # --- HTTP Request ---
27
+
28
+
29
+ def get_http_request() -> Request:
30
+ from fastmcp.server.http import _current_http_request
31
+
32
+ request = _current_http_request.get()
33
+ if request is None:
34
+ raise RuntimeError("No active HTTP request found.")
35
+ return request
fastmcp/server/http.py ADDED
@@ -0,0 +1,309 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncGenerator, Callable, Generator
4
+ from contextlib import asynccontextmanager, contextmanager
5
+ from contextvars import ContextVar
6
+ from typing import TYPE_CHECKING, cast
7
+
8
+ from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
9
+ from mcp.server.auth.middleware.bearer_auth import (
10
+ BearerAuthBackend,
11
+ RequireAuthMiddleware,
12
+ )
13
+ from mcp.server.auth.provider import OAuthAuthorizationServerProvider
14
+ from mcp.server.auth.routes import create_auth_routes
15
+ from mcp.server.auth.settings import AuthSettings
16
+ from mcp.server.sse import SseServerTransport
17
+ from starlette.applications import Starlette
18
+ from starlette.middleware import Middleware
19
+ from starlette.middleware.authentication import AuthenticationMiddleware
20
+ from starlette.requests import Request
21
+ from starlette.responses import Response
22
+ from starlette.routing import Mount, Route
23
+ from starlette.types import Receive, Scope, Send
24
+
25
+ # This import is vendored until it is finalized in the upstream SDK
26
+ from fastmcp.server.streamable_http_manager import StreamableHTTPSessionManager
27
+ from fastmcp.utilities.logging import get_logger
28
+
29
+ if TYPE_CHECKING:
30
+ from fastmcp.server.server import FastMCP
31
+
32
+ logger = get_logger(__name__)
33
+
34
+ _current_http_request: ContextVar[Request | None] = ContextVar(
35
+ "http_request",
36
+ default=None,
37
+ )
38
+
39
+
40
+ @contextmanager
41
+ def set_http_request(request: Request) -> Generator[Request, None, None]:
42
+ token = _current_http_request.set(request)
43
+ try:
44
+ yield request
45
+ finally:
46
+ _current_http_request.reset(token)
47
+
48
+
49
+ class RequestContextMiddleware:
50
+ """
51
+ Middleware that stores each request in a ContextVar
52
+ """
53
+
54
+ def __init__(self, app):
55
+ self.app = app
56
+
57
+ async def __call__(self, scope, receive, send):
58
+ if scope["type"] == "http":
59
+ with set_http_request(Request(scope)):
60
+ await self.app(scope, receive, send)
61
+ else:
62
+ await self.app(scope, receive, send)
63
+
64
+
65
+ def setup_auth_middleware_and_routes(
66
+ auth_server_provider: OAuthAuthorizationServerProvider | None,
67
+ auth_settings: AuthSettings | None,
68
+ ) -> tuple[list[Middleware], list[Route | Mount], list[str]]:
69
+ """Set up authentication middleware and routes if auth is enabled.
70
+
71
+ Args:
72
+ auth_server_provider: The OAuth authorization server provider
73
+ auth_settings: The auth settings
74
+
75
+ Returns:
76
+ Tuple of (middleware, auth_routes, required_scopes)
77
+ """
78
+ middleware: list[Middleware] = []
79
+ auth_routes: list[Route | Mount] = []
80
+ required_scopes: list[str] = []
81
+
82
+ if auth_server_provider:
83
+ if not auth_settings:
84
+ raise ValueError(
85
+ "auth_settings must be provided when auth_server_provider is specified"
86
+ )
87
+
88
+ middleware = [
89
+ Middleware(
90
+ AuthenticationMiddleware,
91
+ backend=BearerAuthBackend(provider=auth_server_provider),
92
+ ),
93
+ Middleware(AuthContextMiddleware),
94
+ ]
95
+
96
+ required_scopes = auth_settings.required_scopes or []
97
+
98
+ auth_routes.extend(
99
+ create_auth_routes(
100
+ provider=auth_server_provider,
101
+ issuer_url=auth_settings.issuer_url,
102
+ service_documentation_url=auth_settings.service_documentation_url,
103
+ client_registration_options=auth_settings.client_registration_options,
104
+ revocation_options=auth_settings.revocation_options,
105
+ )
106
+ )
107
+
108
+ return middleware, auth_routes, required_scopes
109
+
110
+
111
+ def create_base_app(
112
+ routes: list[Route | Mount],
113
+ middleware: list[Middleware],
114
+ debug: bool,
115
+ lifespan: Callable | None = None,
116
+ ) -> Starlette:
117
+ """Create a base Starlette app with common middleware and routes.
118
+
119
+ Args:
120
+ routes: List of routes to include in the app
121
+ middleware: List of middleware to include in the app
122
+ debug: Whether to enable debug mode
123
+ lifespan: Optional lifespan manager for the app
124
+
125
+ Returns:
126
+ A Starlette application
127
+ """
128
+ # Always add RequestContextMiddleware as the outermost middleware
129
+ middleware.append(Middleware(RequestContextMiddleware))
130
+
131
+ # Create the app
132
+ app_kwargs = {
133
+ "debug": debug,
134
+ "routes": routes,
135
+ "middleware": middleware,
136
+ }
137
+
138
+ if lifespan:
139
+ app_kwargs["lifespan"] = lifespan
140
+
141
+ return Starlette(**app_kwargs)
142
+
143
+
144
+ def create_sse_app(
145
+ server: FastMCP,
146
+ message_path: str,
147
+ sse_path: str,
148
+ auth_server_provider: OAuthAuthorizationServerProvider | None = None,
149
+ auth_settings: AuthSettings | None = None,
150
+ debug: bool = False,
151
+ additional_routes: list[Route] | list[Mount] | list[Route | Mount] | None = None,
152
+ ) -> Starlette:
153
+ """Return an instance of the SSE server app.
154
+
155
+ Args:
156
+ server: The FastMCP server instance
157
+ message_path: Path for SSE messages
158
+ sse_path: Path for SSE connections
159
+ auth_server_provider: Optional auth provider
160
+ auth_settings: Optional auth settings
161
+ debug: Whether to enable debug mode
162
+ additional_routes: Optional list of custom routes
163
+
164
+ Returns:
165
+ A Starlette application with RequestContextMiddleware
166
+ """
167
+ # Set up SSE transport
168
+ sse = SseServerTransport(message_path)
169
+
170
+ # Create handler for SSE connections
171
+ async def handle_sse(scope: Scope, receive: Receive, send: Send) -> Response:
172
+ async with sse.connect_sse(scope, receive, send) as streams:
173
+ await server._mcp_server.run(
174
+ streams[0],
175
+ streams[1],
176
+ server._mcp_server.create_initialization_options(),
177
+ )
178
+ return Response()
179
+
180
+ # Get auth middleware and routes
181
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
182
+ auth_server_provider, auth_settings
183
+ )
184
+
185
+ # Initialize routes with auth routes
186
+ routes: list[Route | Mount] = auth_routes.copy()
187
+
188
+ # Add SSE routes with or without auth
189
+ if auth_server_provider:
190
+ # Auth is enabled, wrap endpoints with RequireAuthMiddleware
191
+ routes.append(
192
+ Route(
193
+ sse_path,
194
+ endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
195
+ methods=["GET"],
196
+ )
197
+ )
198
+ routes.append(
199
+ Mount(
200
+ message_path,
201
+ app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
202
+ )
203
+ )
204
+ else:
205
+ # No auth required
206
+ async def sse_endpoint(request: Request) -> Response:
207
+ return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
208
+
209
+ routes.append(
210
+ Route(
211
+ sse_path,
212
+ endpoint=sse_endpoint,
213
+ methods=["GET"],
214
+ )
215
+ )
216
+ routes.append(
217
+ Mount(
218
+ message_path,
219
+ app=sse.handle_post_message,
220
+ )
221
+ )
222
+
223
+ # Add custom routes with lowest precedence
224
+ if additional_routes:
225
+ routes.extend(cast(list[Route | Mount], additional_routes))
226
+
227
+ # Create and return the app
228
+ return create_base_app(routes, middleware, debug)
229
+
230
+
231
+ def create_streamable_http_app(
232
+ server: FastMCP,
233
+ streamable_http_path: str,
234
+ event_store: None = None,
235
+ auth_server_provider: OAuthAuthorizationServerProvider | None = None,
236
+ auth_settings: AuthSettings | None = None,
237
+ json_response: bool = False,
238
+ stateless_http: bool = False,
239
+ debug: bool = False,
240
+ additional_routes: list[Route] | list[Mount] | list[Route | Mount] | None = None,
241
+ ) -> Starlette:
242
+ """Return an instance of the StreamableHTTP server app.
243
+
244
+ Args:
245
+ server: The FastMCP server instance
246
+ streamable_http_path: Path for StreamableHTTP connections
247
+ event_store: Optional event store for session management
248
+ auth_server_provider: Optional auth provider
249
+ auth_settings: Optional auth settings
250
+ json_response: Whether to use JSON response format
251
+ stateless_http: Whether to use stateless mode (new transport per request)
252
+ debug: Whether to enable debug mode
253
+ additional_routes: Optional list of custom routes
254
+
255
+ Returns:
256
+ A Starlette application with StreamableHTTP support
257
+ """
258
+ # Create session manager using the provided event store
259
+ session_manager = StreamableHTTPSessionManager(
260
+ app=server._mcp_server,
261
+ event_store=event_store,
262
+ json_response=json_response,
263
+ stateless=stateless_http,
264
+ )
265
+
266
+ # Create the ASGI handler
267
+ async def handle_streamable_http(
268
+ scope: Scope, receive: Receive, send: Send
269
+ ) -> None:
270
+ await session_manager.handle_request(scope, receive, send)
271
+
272
+ # Get auth middleware and routes
273
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
274
+ auth_server_provider, auth_settings
275
+ )
276
+
277
+ # Initialize routes with auth routes
278
+ routes: list[Route | Mount] = auth_routes.copy()
279
+
280
+ # Add StreamableHTTP routes with or without auth
281
+ if auth_server_provider:
282
+ # Auth is enabled, wrap endpoint with RequireAuthMiddleware
283
+ routes.append(
284
+ Mount(
285
+ streamable_http_path,
286
+ app=RequireAuthMiddleware(handle_streamable_http, required_scopes),
287
+ )
288
+ )
289
+ else:
290
+ # No auth required
291
+ routes.append(
292
+ Mount(
293
+ streamable_http_path,
294
+ app=handle_streamable_http,
295
+ )
296
+ )
297
+
298
+ # Add custom routes with lowest precedence
299
+ if additional_routes:
300
+ routes.extend(cast(list[Route | Mount], additional_routes))
301
+
302
+ # Create a lifespan manager to start and stop the session manager
303
+ @asynccontextmanager
304
+ async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
305
+ async with session_manager.run():
306
+ yield
307
+
308
+ # Create and return the app with lifespan
309
+ return create_base_app(routes, middleware, debug, lifespan)
fastmcp/server/openapi.py CHANGED
@@ -25,9 +25,6 @@ from fastmcp.utilities.openapi import (
25
25
  )
26
26
 
27
27
  if TYPE_CHECKING:
28
- from mcp.server.session import ServerSessionT
29
- from mcp.shared.context import LifespanContextT
30
-
31
28
  from fastmcp.server import Context
32
29
 
33
30
  logger = get_logger(__name__)
@@ -132,7 +129,6 @@ class OpenAPITool(Tool):
132
129
  description=description,
133
130
  parameters=parameters,
134
131
  fn=self._execute_request, # We'll use an instance method instead of a global function
135
- context_kwarg="context", # Default context keyword argument
136
132
  tags=tags,
137
133
  annotations=annotations,
138
134
  serializer=serializer,
@@ -258,12 +254,10 @@ class OpenAPITool(Tool):
258
254
  raise ValueError(f"Request error: {str(e)}")
259
255
 
260
256
  async def run(
261
- self,
262
- arguments: dict[str, Any],
263
- context: Context[ServerSessionT, LifespanContextT] | None = None,
257
+ self, arguments: dict[str, Any]
264
258
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
265
259
  """Run the tool with arguments and optional context."""
266
- response = await self._execute_request(**arguments, context=context)
260
+ response = await self._execute_request(**arguments)
267
261
  return _convert_to_content(response)
268
262
 
269
263
 
@@ -292,9 +286,7 @@ class OpenAPIResource(Resource):
292
286
  self._route = route
293
287
  self._timeout = timeout
294
288
 
295
- async def read(
296
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
297
- ) -> str | bytes:
289
+ async def read(self) -> str | bytes:
298
290
  """Fetch the resource data by making an HTTP request."""
299
291
  try:
300
292
  # Extract path parameters from the URI if present
@@ -399,7 +391,6 @@ class OpenAPIResourceTemplate(ResourceTemplate):
399
391
  fn=lambda **kwargs: None,
400
392
  parameters=parameters,
401
393
  tags=tags,
402
- context_kwarg=None,
403
394
  )
404
395
  self._client = client
405
396
  self._route = route
@@ -409,7 +400,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
409
400
  self,
410
401
  uri: str,
411
402
  params: dict[str, Any],
412
- context: Context[ServerSessionT, LifespanContextT] | None = None,
403
+ context: Context | None = None,
413
404
  ) -> Resource:
414
405
  """Create a resource with the given parameters."""
415
406
  # Generate a URI for this resource instance
@@ -650,7 +641,5 @@ class FastMCPOpenAPI(FastMCP):
650
641
 
651
642
  async def _mcp_call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
652
643
  """Override the call_tool method to return the raw result without converting to content."""
653
-
654
- context = self.get_context()
655
- result = await self._tool_manager.call_tool(name, arguments, context=context)
644
+ result = await self._tool_manager.call_tool(name, arguments)
656
645
  return result
fastmcp/server/proxy.py CHANGED
@@ -27,9 +27,6 @@ from fastmcp.tools.tool import Tool
27
27
  from fastmcp.utilities.logging import get_logger
28
28
 
29
29
  if TYPE_CHECKING:
30
- from mcp.server.session import ServerSessionT
31
- from mcp.shared.context import LifespanContextT
32
-
33
30
  from fastmcp.server import Context
34
31
 
35
32
  logger = get_logger(__name__)
@@ -57,7 +54,7 @@ class ProxyTool(Tool):
57
54
  async def run(
58
55
  self,
59
56
  arguments: dict[str, Any],
60
- context: Context[ServerSessionT, LifespanContextT] | None = None,
57
+ context: Context | None = None,
61
58
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
62
59
  # the client context manager will swallow any exceptions inside a TaskGroup
63
60
  # so we return the raw result and raise an exception ourselves
@@ -89,9 +86,7 @@ class ProxyResource(Resource):
89
86
  mime_type=resource.mimeType,
90
87
  )
91
88
 
92
- async def read(
93
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
94
- ) -> str | bytes:
89
+ async def read(self) -> str | bytes:
95
90
  if self._value is not None:
96
91
  return self._value
97
92
 
@@ -127,7 +122,7 @@ class ProxyTemplate(ResourceTemplate):
127
122
  self,
128
123
  uri: str,
129
124
  params: dict[str, Any],
130
- context: Context[ServerSessionT, LifespanContextT] | None = None,
125
+ context: Context | None = None,
131
126
  ) -> ProxyResource:
132
127
  # dont use the provided uri, because it may not be the same as the
133
128
  # uri_template on the remote server.
@@ -171,11 +166,7 @@ class ProxyPrompt(Prompt):
171
166
  fn=_proxy_passthrough,
172
167
  )
173
168
 
174
- async def render(
175
- self,
176
- arguments: dict[str, Any],
177
- context: Context[ServerSessionT, LifespanContextT] | None = None,
178
- ) -> list[PromptMessage]:
169
+ async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
179
170
  async with self._client:
180
171
  result = await self._client.get_prompt(self.name, arguments)
181
172
  return result.messages