fastmcp 2.2.10__py3-none-any.whl → 2.3.0__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,308 @@
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 mcp.server.streamable_http_manager import StreamableHTTPSessionManager
18
+ from starlette.applications import Starlette
19
+ from starlette.middleware import Middleware
20
+ from starlette.middleware.authentication import AuthenticationMiddleware
21
+ from starlette.requests import Request
22
+ from starlette.responses import Response
23
+ from starlette.routing import Mount, Route
24
+ from starlette.types import Receive, Scope, Send
25
+
26
+ from fastmcp.utilities.logging import get_logger
27
+
28
+ if TYPE_CHECKING:
29
+ from fastmcp.server.server import FastMCP
30
+
31
+ logger = get_logger(__name__)
32
+
33
+ _current_http_request: ContextVar[Request | None] = ContextVar(
34
+ "http_request",
35
+ default=None,
36
+ )
37
+
38
+
39
+ @contextmanager
40
+ def set_http_request(request: Request) -> Generator[Request, None, None]:
41
+ token = _current_http_request.set(request)
42
+ try:
43
+ yield request
44
+ finally:
45
+ _current_http_request.reset(token)
46
+
47
+
48
+ class RequestContextMiddleware:
49
+ """
50
+ Middleware that stores each request in a ContextVar
51
+ """
52
+
53
+ def __init__(self, app):
54
+ self.app = app
55
+
56
+ async def __call__(self, scope, receive, send):
57
+ if scope["type"] == "http":
58
+ with set_http_request(Request(scope)):
59
+ await self.app(scope, receive, send)
60
+ else:
61
+ await self.app(scope, receive, send)
62
+
63
+
64
+ def setup_auth_middleware_and_routes(
65
+ auth_server_provider: OAuthAuthorizationServerProvider | None,
66
+ auth_settings: AuthSettings | None,
67
+ ) -> tuple[list[Middleware], list[Route | Mount], list[str]]:
68
+ """Set up authentication middleware and routes if auth is enabled.
69
+
70
+ Args:
71
+ auth_server_provider: The OAuth authorization server provider
72
+ auth_settings: The auth settings
73
+
74
+ Returns:
75
+ Tuple of (middleware, auth_routes, required_scopes)
76
+ """
77
+ middleware: list[Middleware] = []
78
+ auth_routes: list[Route | Mount] = []
79
+ required_scopes: list[str] = []
80
+
81
+ if auth_server_provider:
82
+ if not auth_settings:
83
+ raise ValueError(
84
+ "auth_settings must be provided when auth_server_provider is specified"
85
+ )
86
+
87
+ middleware = [
88
+ Middleware(
89
+ AuthenticationMiddleware,
90
+ backend=BearerAuthBackend(provider=auth_server_provider),
91
+ ),
92
+ Middleware(AuthContextMiddleware),
93
+ ]
94
+
95
+ required_scopes = auth_settings.required_scopes or []
96
+
97
+ auth_routes.extend(
98
+ create_auth_routes(
99
+ provider=auth_server_provider,
100
+ issuer_url=auth_settings.issuer_url,
101
+ service_documentation_url=auth_settings.service_documentation_url,
102
+ client_registration_options=auth_settings.client_registration_options,
103
+ revocation_options=auth_settings.revocation_options,
104
+ )
105
+ )
106
+
107
+ return middleware, auth_routes, required_scopes
108
+
109
+
110
+ def create_base_app(
111
+ routes: list[Route | Mount],
112
+ middleware: list[Middleware],
113
+ debug: bool,
114
+ lifespan: Callable | None = None,
115
+ ) -> Starlette:
116
+ """Create a base Starlette app with common middleware and routes.
117
+
118
+ Args:
119
+ routes: List of routes to include in the app
120
+ middleware: List of middleware to include in the app
121
+ debug: Whether to enable debug mode
122
+ lifespan: Optional lifespan manager for the app
123
+
124
+ Returns:
125
+ A Starlette application
126
+ """
127
+ # Always add RequestContextMiddleware as the outermost middleware
128
+ middleware.append(Middleware(RequestContextMiddleware))
129
+
130
+ # Create the app
131
+ app_kwargs = {
132
+ "debug": debug,
133
+ "routes": routes,
134
+ "middleware": middleware,
135
+ }
136
+
137
+ if lifespan:
138
+ app_kwargs["lifespan"] = lifespan
139
+
140
+ return Starlette(**app_kwargs)
141
+
142
+
143
+ def create_sse_app(
144
+ server: FastMCP,
145
+ message_path: str,
146
+ sse_path: str,
147
+ auth_server_provider: OAuthAuthorizationServerProvider | None = None,
148
+ auth_settings: AuthSettings | None = None,
149
+ debug: bool = False,
150
+ additional_routes: list[Route] | list[Mount] | list[Route | Mount] | None = None,
151
+ ) -> Starlette:
152
+ """Return an instance of the SSE server app.
153
+
154
+ Args:
155
+ server: The FastMCP server instance
156
+ message_path: Path for SSE messages
157
+ sse_path: Path for SSE connections
158
+ auth_server_provider: Optional auth provider
159
+ auth_settings: Optional auth settings
160
+ debug: Whether to enable debug mode
161
+ additional_routes: Optional list of custom routes
162
+
163
+ Returns:
164
+ A Starlette application with RequestContextMiddleware
165
+ """
166
+ # Set up SSE transport
167
+ sse = SseServerTransport(message_path)
168
+
169
+ # Create handler for SSE connections
170
+ async def handle_sse(scope: Scope, receive: Receive, send: Send) -> Response:
171
+ async with sse.connect_sse(scope, receive, send) as streams:
172
+ await server._mcp_server.run(
173
+ streams[0],
174
+ streams[1],
175
+ server._mcp_server.create_initialization_options(),
176
+ )
177
+ return Response()
178
+
179
+ # Get auth middleware and routes
180
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
181
+ auth_server_provider, auth_settings
182
+ )
183
+
184
+ # Initialize routes with auth routes
185
+ routes: list[Route | Mount] = auth_routes.copy()
186
+
187
+ # Add SSE routes with or without auth
188
+ if auth_server_provider:
189
+ # Auth is enabled, wrap endpoints with RequireAuthMiddleware
190
+ routes.append(
191
+ Route(
192
+ sse_path,
193
+ endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
194
+ methods=["GET"],
195
+ )
196
+ )
197
+ routes.append(
198
+ Mount(
199
+ message_path,
200
+ app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
201
+ )
202
+ )
203
+ else:
204
+ # No auth required
205
+ async def sse_endpoint(request: Request) -> Response:
206
+ return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
207
+
208
+ routes.append(
209
+ Route(
210
+ sse_path,
211
+ endpoint=sse_endpoint,
212
+ methods=["GET"],
213
+ )
214
+ )
215
+ routes.append(
216
+ Mount(
217
+ message_path,
218
+ app=sse.handle_post_message,
219
+ )
220
+ )
221
+
222
+ # Add custom routes with lowest precedence
223
+ if additional_routes:
224
+ routes.extend(cast(list[Route | Mount], additional_routes))
225
+
226
+ # Create and return the app
227
+ return create_base_app(routes, middleware, debug)
228
+
229
+
230
+ def create_streamable_http_app(
231
+ server: FastMCP,
232
+ streamable_http_path: str,
233
+ event_store: None = None,
234
+ auth_server_provider: OAuthAuthorizationServerProvider | None = None,
235
+ auth_settings: AuthSettings | None = None,
236
+ json_response: bool = False,
237
+ stateless_http: bool = False,
238
+ debug: bool = False,
239
+ additional_routes: list[Route] | list[Mount] | list[Route | Mount] | None = None,
240
+ ) -> Starlette:
241
+ """Return an instance of the StreamableHTTP server app.
242
+
243
+ Args:
244
+ server: The FastMCP server instance
245
+ streamable_http_path: Path for StreamableHTTP connections
246
+ event_store: Optional event store for session management
247
+ auth_server_provider: Optional auth provider
248
+ auth_settings: Optional auth settings
249
+ json_response: Whether to use JSON response format
250
+ stateless_http: Whether to use stateless mode (new transport per request)
251
+ debug: Whether to enable debug mode
252
+ additional_routes: Optional list of custom routes
253
+
254
+ Returns:
255
+ A Starlette application with StreamableHTTP support
256
+ """
257
+ # Create session manager using the provided event store
258
+ session_manager = StreamableHTTPSessionManager(
259
+ app=server._mcp_server,
260
+ event_store=event_store,
261
+ json_response=json_response,
262
+ stateless=stateless_http,
263
+ )
264
+
265
+ # Create the ASGI handler
266
+ async def handle_streamable_http(
267
+ scope: Scope, receive: Receive, send: Send
268
+ ) -> None:
269
+ await session_manager.handle_request(scope, receive, send)
270
+
271
+ # Get auth middleware and routes
272
+ middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
273
+ auth_server_provider, auth_settings
274
+ )
275
+
276
+ # Initialize routes with auth routes
277
+ routes: list[Route | Mount] = auth_routes.copy()
278
+
279
+ # Add StreamableHTTP routes with or without auth
280
+ if auth_server_provider:
281
+ # Auth is enabled, wrap endpoint with RequireAuthMiddleware
282
+ routes.append(
283
+ Mount(
284
+ streamable_http_path,
285
+ app=RequireAuthMiddleware(handle_streamable_http, required_scopes),
286
+ )
287
+ )
288
+ else:
289
+ # No auth required
290
+ routes.append(
291
+ Mount(
292
+ streamable_http_path,
293
+ app=handle_streamable_http,
294
+ )
295
+ )
296
+
297
+ # Add custom routes with lowest precedence
298
+ if additional_routes:
299
+ routes.extend(cast(list[Route | Mount], additional_routes))
300
+
301
+ # Create a lifespan manager to start and stop the session manager
302
+ @asynccontextmanager
303
+ async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
304
+ async with session_manager.run():
305
+ yield
306
+
307
+ # Create and return the app with lifespan
308
+ 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