Add support for starlette request

This commit is contained in:
Jeremiah Lowin 2025-05-02 17:16:43 -04:00
commit b672468d4f
4 changed files with 77 additions and 37 deletions

View file

@ -269,11 +269,31 @@ async def advanced_tool(ctx: Context) -> str:
return f"Server: {server_name}"
```
For web applications, you can access the underlying HTTP request:
```python
@mcp.tool()
async def handle_web_request(ctx: Context) -> dict:
"""Access HTTP request information from the Starlette request."""
request = ctx.get_starlette_request()
# Access HTTP headers, query parameters, etc.
user_agent = request.headers.get("user-agent", "Unknown")
client_ip = request.client.host if request.client else "Unknown"
return {
"user_agent": user_agent,
"client_ip": client_ip,
"path": request.url.path,
}
```
**Advanced Properties:**
- **`ctx.fastmcp -> FastMCP`**: Access the server instance the context belongs to
- **`ctx.session`**: Access the raw `mcp.server.session.ServerSession` object
- **`ctx.request_context`**: Access the raw `mcp.shared.context.RequestContext` object
- **`ctx.get_starlette_request() -> Request`**: Access the active Starlette request object (when running with a web server)
<Warning>
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.

View file

@ -18,6 +18,7 @@ from pydantic.networks import AnyUrl
from starlette.requests import Request
from fastmcp.server.server import FastMCP
from fastmcp.utilities.http import get_current_starlette_request
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
@ -59,7 +60,6 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
_request_context: RequestContext[ServerSessionT, LifespanContextT] | None
_fastmcp: FastMCP | None
_request: Request | None
model_config = ConfigDict(arbitrary_types_allowed=True)
@ -68,13 +68,11 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
*,
request_context: RequestContext[ServerSessionT, LifespanContextT] | None = None,
fastmcp: FastMCP | None = None,
request: Request | None = None,
**kwargs: Any,
):
super().__init__(**kwargs)
self._request_context = request_context
self._fastmcp = fastmcp
self._request = request
@property
def fastmcp(self) -> FastMCP:
@ -90,13 +88,6 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
raise ValueError("Context is not available outside of a request")
return self._request_context
@property
def request(self) -> Request:
"""Access to the underlying request."""
if self._request is None:
raise ValueError("Context is not available outside of a request")
return self._request
async def report_progress(
self, progress: float, total: float | None = None
) -> None:
@ -235,3 +226,10 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
)
return result.content
def get_starlette_request(self) -> Request:
"""Get the active starlette request."""
request = get_current_starlette_request()
if request is None:
raise ValueError("Request is not available outside a Starlette request")
return request

View file

@ -9,7 +9,6 @@ from contextlib import (
AsyncExitStack,
asynccontextmanager,
)
from contextvars import ContextVar
from functools import partial
from typing import TYPE_CHECKING, Any, Generic, Literal
@ -60,6 +59,7 @@ from fastmcp.resources.template import ResourceTemplate
from fastmcp.tools import ToolManager
from fastmcp.tools.tool import Tool
from fastmcp.utilities.decorators import DecoratedFunction
from fastmcp.utilities.http import RequestMiddleware
from fastmcp.utilities.logging import configure_logging, get_logger
if TYPE_CHECKING:
@ -73,25 +73,6 @@ logger = get_logger(__name__)
NOT_FOUND = object()
_current_starlette_request: ContextVar[Request | None] = ContextVar(
"starlette_request",
default=None,
)
@asynccontextmanager
async def starlette_request_context(request: Request):
token = _current_starlette_request.set(request)
try:
yield
finally:
_current_starlette_request.reset(token)
def get_current_starlette_request() -> Request | None:
return _current_starlette_request.get()
class MountedServer:
def __init__(
self,
@ -333,11 +314,7 @@ class FastMCP(Generic[LifespanResultT]):
request_context = None
from fastmcp.server.context import Context
return Context(
request_context=request_context,
fastmcp=self,
request=get_current_starlette_request(),
)
return Context(request_context=request_context, fastmcp=self)
async def get_tools(self) -> dict[str, Tool]:
"""Get all registered tools, indexed by registered key."""
@ -846,10 +823,11 @@ class FastMCP(Generic[LifespanResultT]):
log_level: str | None = None,
) -> None:
"""Run the server using SSE transport."""
starlette_app = self.sse_app()
app = self.sse_app()
app = RequestMiddleware(app)
config = uvicorn.Config(
starlette_app,
app,
host=host or self.settings.host,
port=port or self.settings.port,
log_level=log_level or self.settings.log_level.lower(),

View file

@ -0,0 +1,44 @@
from __future__ import annotations
from contextlib import (
asynccontextmanager,
)
from contextvars import ContextVar
from starlette.requests import Request
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
_current_starlette_request: ContextVar[Request | None] = ContextVar(
"starlette_request",
default=None,
)
@asynccontextmanager
async def starlette_request_context(request: Request):
token = _current_starlette_request.set(request)
try:
yield
finally:
_current_starlette_request.reset(token)
def get_current_starlette_request() -> Request | None:
return _current_starlette_request.get()
class RequestMiddleware:
"""
Middleware that stores each request in a ContextVar
"""
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
async with starlette_request_context(Request(scope)):
await self.app(scope, receive, send)