Skip to content

Commit 23acc82

Browse files
authored
Merge pull request jlowin#302 from jlowin/context
Add method for retrieving current starlette request to FastMCP context
2 parents b711519 + 6793ee2 commit 23acc82

File tree

4 files changed

+80
-3
lines changed

4 files changed

+80
-3
lines changed

docs/servers/context.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,11 +269,31 @@ async def advanced_tool(ctx: Context) -> str:
269269
return f"Server: {server_name}"
270270
```
271271

272+
For web applications, you can access the underlying HTTP request:
273+
274+
```python
275+
@mcp.tool()
276+
async def handle_web_request(ctx: Context) -> dict:
277+
"""Access HTTP request information from the Starlette request."""
278+
request = ctx.get_starlette_request()
279+
280+
# Access HTTP headers, query parameters, etc.
281+
user_agent = request.headers.get("user-agent", "Unknown")
282+
client_ip = request.client.host if request.client else "Unknown"
283+
284+
return {
285+
"user_agent": user_agent,
286+
"client_ip": client_ip,
287+
"path": request.url.path,
288+
}
289+
```
290+
272291
**Advanced Properties:**
273292

274293
- **`ctx.fastmcp -> FastMCP`**: Access the server instance the context belongs to
275294
- **`ctx.session`**: Access the raw `mcp.server.session.ServerSession` object
276295
- **`ctx.request_context`**: Access the raw `mcp.shared.context.RequestContext` object
296+
- **`ctx.get_starlette_request() -> Request`**: Access the active Starlette request object (when running with a web server)
277297

278298
<Warning>
279299
Direct use of `session` or `request_context` requires understanding the low-level MCP Python SDK and may be less stable than using the methods provided directly on the `Context` object.

src/fastmcp/server/context.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
SamplingMessage,
1414
TextContent,
1515
)
16-
from pydantic import BaseModel
16+
from pydantic import BaseModel, ConfigDict
1717
from pydantic.networks import AnyUrl
18+
from starlette.requests import Request
1819

1920
from fastmcp.server.server import FastMCP
21+
from fastmcp.utilities.http import get_current_starlette_request
2022
from fastmcp.utilities.logging import get_logger
2123

2224
logger = get_logger(__name__)
@@ -59,6 +61,8 @@ def my_tool(x: int, ctx: Context) -> str:
5961
_request_context: RequestContext[ServerSessionT, LifespanContextT] | None
6062
_fastmcp: FastMCP | None
6163

64+
model_config = ConfigDict(arbitrary_types_allowed=True)
65+
6266
def __init__(
6367
self,
6468
*,
@@ -222,3 +226,10 @@ async def sample(
222226
)
223227

224228
return result.content
229+
230+
def get_starlette_request(self) -> Request:
231+
"""Get the active starlette request."""
232+
request = get_current_starlette_request()
233+
if request is None:
234+
raise ValueError("Request is not available outside a Starlette request")
235+
return request

src/fastmcp/server/server.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from fastmcp.tools import ToolManager
6060
from fastmcp.tools.tool import Tool
6161
from fastmcp.utilities.decorators import DecoratedFunction
62+
from fastmcp.utilities.http import RequestMiddleware
6263
from fastmcp.utilities.logging import configure_logging, get_logger
6364

6465
if TYPE_CHECKING:
@@ -822,10 +823,11 @@ async def run_sse_async(
822823
log_level: str | None = None,
823824
) -> None:
824825
"""Run the server using SSE transport."""
825-
starlette_app = self.sse_app()
826+
app = self.sse_app()
827+
app = RequestMiddleware(app)
826828

827829
config = uvicorn.Config(
828-
starlette_app,
830+
app,
829831
host=host or self.settings.host,
830832
port=port or self.settings.port,
831833
log_level=log_level or self.settings.log_level.lower(),

src/fastmcp/utilities/http.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from __future__ import annotations
2+
3+
from contextlib import (
4+
asynccontextmanager,
5+
)
6+
from contextvars import ContextVar
7+
8+
from starlette.requests import Request
9+
10+
from fastmcp.utilities.logging import get_logger
11+
12+
logger = get_logger(__name__)
13+
14+
15+
_current_starlette_request: ContextVar[Request | None] = ContextVar(
16+
"starlette_request",
17+
default=None,
18+
)
19+
20+
21+
@asynccontextmanager
22+
async def starlette_request_context(request: Request):
23+
token = _current_starlette_request.set(request)
24+
try:
25+
yield
26+
finally:
27+
_current_starlette_request.reset(token)
28+
29+
30+
def get_current_starlette_request() -> Request | None:
31+
return _current_starlette_request.get()
32+
33+
34+
class RequestMiddleware:
35+
"""
36+
Middleware that stores each request in a ContextVar
37+
"""
38+
39+
def __init__(self, app):
40+
self.app = app
41+
42+
async def __call__(self, scope, receive, send):
43+
async with starlette_request_context(Request(scope)):
44+
await self.app(scope, receive, send)

0 commit comments

Comments
 (0)