mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
Merge branch 'main' into oauthclient
This commit is contained in:
commit
7411a72872
10 changed files with 116 additions and 30 deletions
29
.github/labeler.yml
vendored
Normal file
29
.github/labeler.yml
vendored
Normal 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
12
.github/workflows/labeler.yml
vendored
Normal 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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
26
tests/server/test_app_state.py
Normal file
26
tests/server/test_app_state.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue