Merge branch 'main' into oauthclient

This commit is contained in:
Jeremiah Lowin 2025-05-17 20:10:42 -04:00
commit 7411a72872
10 changed files with 116 additions and 30 deletions

29
.github/labeler.yml vendored Normal file
View file

@ -0,0 +1,29 @@
documentation:
- changed-files:
- any-glob-to-any-file: "docs/**"
example:
- changed-files:
- any-glob-to-any-file:
- "examples/**"
tests:
- changed-files:
- any-glob-to-any-file: "tests/**"
"component: HTTP":
- changed-files:
- any-glob-to-any-file: "src/fastmcp/server/http.py"
"component: client":
- changed-files:
- any-glob-to-any-file: "src/fastmcp/client/**"
"contrib":
- changed-files:
- any-glob-to-any-file: "src/fastmcp/contrib/**"
"component: openapi":
- changed-files:
- any-glob-to-any-file:
- "src/**/*openapi*.py"

12
.github/workflows/labeler.yml vendored Normal file
View file

@ -0,0 +1,12 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5

View file

@ -43,7 +43,10 @@ http_app = mcp.http_app()
sse_app = mcp.http_app(transport="sse")
```
Both approaches return a Starlette application that can be integrated with other ASGI-compatible web frameworks.
Both approaches return a Starlette application that can be integrated with other ASGI-compatible web frameworks.
The returned app stores the `FastMCP` instance on `app.state.fastmcp_server`, so you
can access it from custom middleware or routes via `request.app.state.fastmcp_server`.
The MCP server's endpoint is mounted at the root path `/mcp` for Streamable HTTP transport, and `/sse` for SSE transport, though you can change these paths by passing a `path` argument to the `http_app()` method:
@ -124,7 +127,7 @@ app = Starlette(
Mount("/mcp-server", app=mcp_app),
# Add other routes as needed
],
lifespan=mcp_app.router.lifespan_context,
lifespan=mcp_app.lifespan,
)
```
@ -154,7 +157,7 @@ mcp_app = mcp.http_app(path='/mcp')
inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
app = Starlette(
routes=[Mount("/outer", app=inner_app)],
lifespan=mcp_app.router.lifespan_context,
lifespan=mcp_app.lifespan,
)
```
@ -181,7 +184,7 @@ mcp = FastMCP("MyServer")
mcp_app = mcp.http_app(path='/mcp')
# Create a FastAPI app and mount the MCP server
app = FastAPI(lifespan=mcp_app.router.lifespan_context)
app = FastAPI(lifespan=mcp_app.lifespan)
app.mount("/mcp-server", mcp_app)
```
@ -199,13 +202,13 @@ In addition to adding your FastMCP server to an existing ASGI app, you can also
```python
from fastmcp import FastMCP
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.responses import PlainTextResponse
mcp = FastMCP("MyServer")
@mcp.custom_route("/health", methods=["GET"])
async def health_check(request: Request) -> JSONResponse:
return JSONResponse({"status": "healthy"})
async def health_check(request: Request) -> PlainTextResponse:
return PlainTextResponse("OK")
```
These routes will be included in the FastMCP app when mounted in your web application.
These routes will be included in the FastMCP app when mounted in your web application.

View file

@ -29,7 +29,7 @@ def hello(name: str) -> str:
if __name__ == "__main__":
mcp.run()
```
You can now run this MCP server by executing `python my_server.py`.
You can now run this MCP server by executing `python my_server.py`.
MCP servers can be run with a variety of different transport options, depending on your application's requirements. The `run()` method can take a `transport` argument and other transport-specific keyword arguments to configure how the server operates.
@ -260,13 +260,13 @@ You can also add custom web routes to your FastMCP server, which will be exposed
```python
from fastmcp import FastMCP
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.responses import PlainTextResponse
mcp = FastMCP("MyServer")
@mcp.custom_route("/health", methods=["GET"])
async def health_check(request: Request) -> JSONResponse:
return JSONResponse({"status": "healthy"})
async def health_check(request: Request) -> PlainTextResponse:
return PlainTextResponse("OK")
if __name__ == "__main__":
mcp.run()

View file

@ -72,7 +72,7 @@ There are a few things to note here:
In order to run the server with Python, we need to add a `run` statement to the `__main__` block of the server file.
```python my_server.py {9-10}
from fastmcp import FastMCP, Client
from fastmcp import FastMCP
mcp = FastMCP("My MCP Server")
@ -99,6 +99,7 @@ Now that the server can be executed with `python my_server.py`, we can interact
In a new file, create a client and point it at the server file:
```python my_client.py
import asyncio
from fastmcp import Client
client = Client("my_server.py")

View file

@ -27,7 +27,7 @@ from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import BaseRoute, Mount, Route
from starlette.types import Receive, Scope, Send
from starlette.types import Lifespan, Receive, Scope, Send
from fastmcp.utilities.logging import get_logger
@ -43,6 +43,12 @@ _current_http_request: ContextVar[Request | None] = ContextVar(
)
class StarletteWithLifespan(Starlette):
@property
def lifespan(self) -> Lifespan:
return self.router.lifespan_context
@contextmanager
def set_http_request(request: Request) -> Generator[Request, None, None]:
token = _current_http_request.set(request)
@ -122,7 +128,7 @@ def create_base_app(
middleware: list[Middleware],
debug: bool = False,
lifespan: Callable | None = None,
) -> Starlette:
) -> StarletteWithLifespan:
"""Create a base Starlette app with common middleware and routes.
Args:
@ -137,7 +143,7 @@ def create_base_app(
# Always add RequestContextMiddleware as the outermost middleware
middleware.append(Middleware(RequestContextMiddleware))
return Starlette(
return StarletteWithLifespan(
routes=routes,
middleware=middleware,
debug=debug,
@ -157,7 +163,7 @@ def create_sse_app(
debug: bool = False,
routes: list[BaseRoute] | None = None,
middleware: list[Middleware] | None = None,
) -> Starlette:
) -> StarletteWithLifespan:
"""Return an instance of the SSE server app.
Args:
@ -241,11 +247,15 @@ def create_sse_app(
server_middleware.extend(middleware)
# Create and return the app
return create_base_app(
app = create_base_app(
routes=server_routes,
middleware=server_middleware,
debug=debug,
)
# Store the FastMCP server instance on the Starlette app state
app.state.fastmcp_server = server
return app
def create_streamable_http_app(
@ -262,7 +272,7 @@ def create_streamable_http_app(
debug: bool = False,
routes: list[BaseRoute] | None = None,
middleware: list[Middleware] | None = None,
) -> Starlette:
) -> StarletteWithLifespan:
"""Return an instance of the StreamableHTTP server app.
Args:
@ -338,9 +348,13 @@ def create_streamable_http_app(
yield
# Create and return the app with lifespan
return create_base_app(
app = create_base_app(
routes=server_routes,
middleware=server_middleware,
debug=debug,
lifespan=lifespan,
)
# Store the FastMCP server instance on the Starlette app state
app.state.fastmcp_server = server
return app

View file

@ -35,7 +35,6 @@ from mcp.types import Resource as MCPResource
from mcp.types import ResourceTemplate as MCPResourceTemplate
from mcp.types import Tool as MCPTool
from pydantic import AnyUrl
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import Response
@ -48,7 +47,11 @@ from fastmcp.prompts import Prompt, PromptManager
from fastmcp.prompts.prompt import PromptResult
from fastmcp.resources import Resource, ResourceManager
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.http import create_sse_app
from fastmcp.server.http import (
StarletteWithLifespan,
create_sse_app,
create_streamable_http_app,
)
from fastmcp.tools import ToolManager
from fastmcp.tools.tool import Tool
from fastmcp.utilities.cache import TimedCache
@ -59,7 +62,6 @@ if TYPE_CHECKING:
from fastmcp.client import Client
from fastmcp.server.openapi import FastMCPOpenAPI
from fastmcp.server.proxy import FastMCPProxy
logger = get_logger(__name__)
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
@ -806,7 +808,7 @@ class FastMCP(Generic[LifespanResultT]):
path: str | None = None,
message_path: str | None = None,
middleware: list[Middleware] | None = None,
) -> Starlette:
) -> StarletteWithLifespan:
"""
Create a Starlette app for the SSE server.
@ -837,7 +839,7 @@ class FastMCP(Generic[LifespanResultT]):
self,
path: str | None = None,
middleware: list[Middleware] | None = None,
) -> Starlette:
) -> StarletteWithLifespan:
"""
Create a Starlette app for the StreamableHTTP server.
@ -858,7 +860,7 @@ class FastMCP(Generic[LifespanResultT]):
path: str | None = None,
middleware: list[Middleware] | None = None,
transport: Literal["streamable-http", "sse"] = "streamable-http",
) -> Starlette:
) -> StarletteWithLifespan:
"""Create a Starlette app using the specified HTTP transport.
Args:
@ -869,7 +871,6 @@ class FastMCP(Generic[LifespanResultT]):
Returns:
A Starlette application configured with the specified transport
"""
from fastmcp.server.http import create_streamable_http_app
if transport == "streamable-http":
return create_streamable_http_app(

View file

@ -512,11 +512,11 @@ class TestErrorHandling:
class TestTimeout:
async def test_timeout(self, fastmcp_server: FastMCP):
async with Client(
transport=FastMCPTransport(fastmcp_server), timeout=0.01
transport=FastMCPTransport(fastmcp_server), timeout=0.05
) as client:
with pytest.raises(
McpError,
match="Timed out while waiting for response to ClientRequest. Waited 0.01 seconds",
match="Timed out while waiting for response to ClientRequest. Waited 0.05 seconds",
):
await client.call_tool("sleep", {"seconds": 0.1})

View file

@ -119,7 +119,7 @@ def run_nested_server(host: str, port: int) -> None:
mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
mount2 = Starlette(
routes=[Mount("/nest-outer", app=mount)],
lifespan=mcp_app.router.lifespan_context,
lifespan=mcp_app.lifespan,
)
server = uvicorn.Server(
config=uvicorn.Config(

View file

@ -0,0 +1,26 @@
from fastmcp.server import FastMCP
from fastmcp.server.http import create_sse_app, create_streamable_http_app
def test_http_app_sets_mcp_server_state():
server = FastMCP(name="StateTest")
app = server.http_app()
assert app.state.fastmcp_server is server
def test_http_app_sse_sets_mcp_server_state():
server = FastMCP(name="StateTest")
app = server.http_app(transport="sse")
assert app.state.fastmcp_server is server
def test_create_streamable_http_app_sets_state():
server = FastMCP(name="StateTest")
app = create_streamable_http_app(server, "/mcp")
assert app.state.fastmcp_server is server
def test_create_sse_app_sets_state():
server = FastMCP(name="StateTest")
app = create_sse_app(server, message_path="/message", sse_path="/sse")
assert app.state.fastmcp_server is server