Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
154abca
1
Parent(s): 9a8ac2f
Move sse app to http
Browse files- src/fastmcp/server/http.py +138 -0
- src/fastmcp/server/server.py +12 -111
src/fastmcp/server/http.py
CHANGED
|
@@ -3,11 +3,30 @@ from __future__ import annotations
|
|
| 3 |
from collections.abc import Generator
|
| 4 |
from contextlib import contextmanager
|
| 5 |
from contextvars import ContextVar
|
|
|
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from starlette.requests import Request
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
from fastmcp.utilities.logging import get_logger
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
logger = get_logger(__name__)
|
| 12 |
|
| 13 |
_current_http_request: ContextVar[Request | None] = ContextVar(
|
|
@@ -36,3 +55,122 @@ class RequestContextMiddleware:
|
|
| 36 |
async def __call__(self, scope, receive, send):
|
| 37 |
with set_http_request(Request(scope)):
|
| 38 |
await self.app(scope, receive, send)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from collections.abc import Generator
|
| 4 |
from contextlib import contextmanager
|
| 5 |
from contextvars import ContextVar
|
| 6 |
+
from typing import TYPE_CHECKING
|
| 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 starlette.applications import Starlette
|
| 18 |
+
from starlette.middleware import Middleware
|
| 19 |
+
from starlette.middleware.authentication import AuthenticationMiddleware
|
| 20 |
from starlette.requests import Request
|
| 21 |
+
from starlette.responses import Response
|
| 22 |
+
from starlette.routing import Mount, Route
|
| 23 |
+
from starlette.types import Receive, Scope, Send
|
| 24 |
|
| 25 |
from fastmcp.utilities.logging import get_logger
|
| 26 |
|
| 27 |
+
if TYPE_CHECKING:
|
| 28 |
+
from fastmcp import FastMCP
|
| 29 |
+
|
| 30 |
logger = get_logger(__name__)
|
| 31 |
|
| 32 |
_current_http_request: ContextVar[Request | None] = ContextVar(
|
|
|
|
| 55 |
async def __call__(self, scope, receive, send):
|
| 56 |
with set_http_request(Request(scope)):
|
| 57 |
await self.app(scope, receive, send)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def create_sse_app(
|
| 61 |
+
server: FastMCP,
|
| 62 |
+
message_path: str,
|
| 63 |
+
sse_path: str,
|
| 64 |
+
auth_server_provider: OAuthAuthorizationServerProvider | None = None,
|
| 65 |
+
auth_settings: AuthSettings | None = None,
|
| 66 |
+
debug: bool = False,
|
| 67 |
+
additional_routes: list[Route] | list[Mount] | list[Route | Mount] | None = None,
|
| 68 |
+
) -> Starlette:
|
| 69 |
+
"""Return an instance of the SSE server app.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
server: The FastMCP server instance
|
| 73 |
+
message_path: Path for SSE messages
|
| 74 |
+
sse_path: Path for SSE connections
|
| 75 |
+
auth_server_provider: Optional auth provider
|
| 76 |
+
auth_settings: Optional auth settings
|
| 77 |
+
debug: Whether to enable debug mode
|
| 78 |
+
additional_routes: Optional list of custom routes
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
A Starlette application configured for SSE
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
# Set up SSE transport
|
| 85 |
+
sse = SseServerTransport(message_path)
|
| 86 |
+
|
| 87 |
+
async def handle_sse(scope: Scope, receive: Receive, send: Send):
|
| 88 |
+
# Add client ID from auth context into request context if available
|
| 89 |
+
async with sse.connect_sse(
|
| 90 |
+
scope,
|
| 91 |
+
receive,
|
| 92 |
+
send,
|
| 93 |
+
) as streams:
|
| 94 |
+
await server._mcp_server.run(
|
| 95 |
+
streams[0],
|
| 96 |
+
streams[1],
|
| 97 |
+
server._mcp_server.create_initialization_options(),
|
| 98 |
+
)
|
| 99 |
+
return Response()
|
| 100 |
+
|
| 101 |
+
# Create routes
|
| 102 |
+
routes: list[Route | Mount] = []
|
| 103 |
+
middleware: list[Middleware] = []
|
| 104 |
+
required_scopes = []
|
| 105 |
+
|
| 106 |
+
# Add auth endpoints if auth provider is configured
|
| 107 |
+
if auth_server_provider:
|
| 108 |
+
assert auth_settings
|
| 109 |
+
|
| 110 |
+
required_scopes = auth_settings.required_scopes or []
|
| 111 |
+
|
| 112 |
+
middleware = [
|
| 113 |
+
# extract auth info from request (but do not require it)
|
| 114 |
+
Middleware(
|
| 115 |
+
AuthenticationMiddleware,
|
| 116 |
+
backend=BearerAuthBackend(
|
| 117 |
+
provider=auth_server_provider,
|
| 118 |
+
),
|
| 119 |
+
),
|
| 120 |
+
# Add the auth context middleware to store
|
| 121 |
+
# authenticated user in a contextvar
|
| 122 |
+
Middleware(AuthContextMiddleware),
|
| 123 |
+
]
|
| 124 |
+
routes.extend(
|
| 125 |
+
create_auth_routes(
|
| 126 |
+
provider=auth_server_provider,
|
| 127 |
+
issuer_url=auth_settings.issuer_url,
|
| 128 |
+
service_documentation_url=auth_settings.service_documentation_url,
|
| 129 |
+
client_registration_options=auth_settings.client_registration_options,
|
| 130 |
+
revocation_options=auth_settings.revocation_options,
|
| 131 |
+
)
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# When auth is not configured, we shouldn't require auth
|
| 135 |
+
if auth_server_provider:
|
| 136 |
+
# Auth is enabled, wrap the endpoints with RequireAuthMiddleware
|
| 137 |
+
routes.append(
|
| 138 |
+
Route(
|
| 139 |
+
sse_path,
|
| 140 |
+
endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
|
| 141 |
+
methods=["GET"],
|
| 142 |
+
)
|
| 143 |
+
)
|
| 144 |
+
routes.append(
|
| 145 |
+
Mount(
|
| 146 |
+
message_path,
|
| 147 |
+
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
|
| 148 |
+
)
|
| 149 |
+
)
|
| 150 |
+
else:
|
| 151 |
+
# Auth is disabled, no need for RequireAuthMiddleware
|
| 152 |
+
# Since handle_sse is an ASGI app, we need to create a compatible endpoint
|
| 153 |
+
async def sse_endpoint(request: Request) -> None:
|
| 154 |
+
# Convert the Starlette request to ASGI parameters
|
| 155 |
+
await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
|
| 156 |
+
|
| 157 |
+
routes.append(
|
| 158 |
+
Route(
|
| 159 |
+
sse_path,
|
| 160 |
+
endpoint=sse_endpoint,
|
| 161 |
+
methods=["GET"],
|
| 162 |
+
)
|
| 163 |
+
)
|
| 164 |
+
routes.append(
|
| 165 |
+
Mount(
|
| 166 |
+
message_path,
|
| 167 |
+
app=sse.handle_post_message,
|
| 168 |
+
)
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# mount custom routes last, so they have the lowest route matching precedence
|
| 172 |
+
if additional_routes:
|
| 173 |
+
routes.extend(additional_routes)
|
| 174 |
+
|
| 175 |
+
# Create Starlette app with routes and middleware
|
| 176 |
+
return Starlette(debug=debug, routes=routes, middleware=middleware)
|
src/fastmcp/server/server.py
CHANGED
|
@@ -16,16 +16,10 @@ import anyio
|
|
| 16 |
import httpx
|
| 17 |
import pydantic
|
| 18 |
import uvicorn
|
| 19 |
-
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
| 20 |
-
from mcp.server.auth.middleware.bearer_auth import (
|
| 21 |
-
BearerAuthBackend,
|
| 22 |
-
RequireAuthMiddleware,
|
| 23 |
-
)
|
| 24 |
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
|
| 25 |
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
| 26 |
from mcp.server.lowlevel.server import LifespanResultT
|
| 27 |
from mcp.server.lowlevel.server import Server as MCPServer
|
| 28 |
-
from mcp.server.sse import SseServerTransport
|
| 29 |
from mcp.server.stdio import stdio_server
|
| 30 |
from mcp.types import (
|
| 31 |
AnyFunction,
|
|
@@ -41,12 +35,9 @@ from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
| 41 |
from mcp.types import Tool as MCPTool
|
| 42 |
from pydantic import AnyUrl
|
| 43 |
from starlette.applications import Starlette
|
| 44 |
-
from starlette.middleware import Middleware
|
| 45 |
-
from starlette.middleware.authentication import AuthenticationMiddleware
|
| 46 |
from starlette.requests import Request
|
| 47 |
from starlette.responses import Response
|
| 48 |
-
from starlette.routing import
|
| 49 |
-
from starlette.types import Receive, Scope, Send
|
| 50 |
|
| 51 |
import fastmcp.server
|
| 52 |
import fastmcp.settings
|
|
@@ -55,7 +46,7 @@ from fastmcp.prompts import Prompt, PromptManager
|
|
| 55 |
from fastmcp.prompts.prompt import PromptResult
|
| 56 |
from fastmcp.resources import Resource, ResourceManager
|
| 57 |
from fastmcp.resources.template import ResourceTemplate
|
| 58 |
-
from fastmcp.server.http import RequestContextMiddleware
|
| 59 |
from fastmcp.tools import ToolManager
|
| 60 |
from fastmcp.tools.tool import Tool
|
| 61 |
from fastmcp.utilities.cache import TimedCache
|
|
@@ -155,7 +146,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 155 |
"is specified"
|
| 156 |
)
|
| 157 |
self._auth_server_provider = auth_server_provider
|
| 158 |
-
self.
|
| 159 |
self.dependencies = self.settings.dependencies
|
| 160 |
|
| 161 |
# Set up MCP protocol handlers
|
|
@@ -294,7 +285,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 294 |
def decorator(
|
| 295 |
func: Callable[[Request], Awaitable[Response]],
|
| 296 |
) -> Callable[[Request], Awaitable[Response]]:
|
| 297 |
-
self.
|
| 298 |
Route(
|
| 299 |
path,
|
| 300 |
endpoint=func,
|
|
@@ -742,104 +733,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 742 |
|
| 743 |
def sse_app(self) -> Starlette:
|
| 744 |
"""Return an instance of the SSE server app."""
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
# Add client ID from auth context into request context if available
|
| 754 |
-
|
| 755 |
-
async with sse.connect_sse(
|
| 756 |
-
scope,
|
| 757 |
-
receive,
|
| 758 |
-
send,
|
| 759 |
-
) as streams:
|
| 760 |
-
await self._mcp_server.run(
|
| 761 |
-
streams[0],
|
| 762 |
-
streams[1],
|
| 763 |
-
self._mcp_server.create_initialization_options(),
|
| 764 |
-
)
|
| 765 |
-
return Response()
|
| 766 |
-
|
| 767 |
-
# Create routes
|
| 768 |
-
routes: list[Route | Mount] = []
|
| 769 |
-
middleware: list[Middleware] = []
|
| 770 |
-
required_scopes = []
|
| 771 |
-
|
| 772 |
-
# Add auth endpoints if auth provider is configured
|
| 773 |
-
if self._auth_server_provider:
|
| 774 |
-
assert self.settings.auth
|
| 775 |
-
from mcp.server.auth.routes import create_auth_routes
|
| 776 |
-
|
| 777 |
-
required_scopes = self.settings.auth.required_scopes or []
|
| 778 |
-
|
| 779 |
-
middleware = [
|
| 780 |
-
# extract auth info from request (but do not require it)
|
| 781 |
-
Middleware(
|
| 782 |
-
AuthenticationMiddleware,
|
| 783 |
-
backend=BearerAuthBackend(
|
| 784 |
-
provider=self._auth_server_provider,
|
| 785 |
-
),
|
| 786 |
-
),
|
| 787 |
-
# Add the auth context middleware to store
|
| 788 |
-
# authenticated user in a contextvar
|
| 789 |
-
Middleware(AuthContextMiddleware),
|
| 790 |
-
]
|
| 791 |
-
routes.extend(
|
| 792 |
-
create_auth_routes(
|
| 793 |
-
provider=self._auth_server_provider,
|
| 794 |
-
issuer_url=self.settings.auth.issuer_url,
|
| 795 |
-
service_documentation_url=self.settings.auth.service_documentation_url,
|
| 796 |
-
client_registration_options=self.settings.auth.client_registration_options,
|
| 797 |
-
revocation_options=self.settings.auth.revocation_options,
|
| 798 |
-
)
|
| 799 |
-
)
|
| 800 |
-
|
| 801 |
-
# When auth is not configured, we shouldn't require auth
|
| 802 |
-
if self._auth_server_provider:
|
| 803 |
-
# Auth is enabled, wrap the endpoints with RequireAuthMiddleware
|
| 804 |
-
routes.append(
|
| 805 |
-
Route(
|
| 806 |
-
self.settings.sse_path,
|
| 807 |
-
endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
|
| 808 |
-
methods=["GET"],
|
| 809 |
-
)
|
| 810 |
-
)
|
| 811 |
-
routes.append(
|
| 812 |
-
Mount(
|
| 813 |
-
self.settings.message_path,
|
| 814 |
-
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
|
| 815 |
-
)
|
| 816 |
-
)
|
| 817 |
-
else:
|
| 818 |
-
# Auth is disabled, no need for RequireAuthMiddleware
|
| 819 |
-
# Since handle_sse is an ASGI app, we need to create a compatible endpoint
|
| 820 |
-
async def sse_endpoint(request: Request) -> None:
|
| 821 |
-
# Convert the Starlette request to ASGI parameters
|
| 822 |
-
await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
|
| 823 |
-
|
| 824 |
-
routes.append(
|
| 825 |
-
Route(
|
| 826 |
-
self.settings.sse_path,
|
| 827 |
-
endpoint=sse_endpoint,
|
| 828 |
-
methods=["GET"],
|
| 829 |
-
)
|
| 830 |
-
)
|
| 831 |
-
routes.append(
|
| 832 |
-
Mount(
|
| 833 |
-
self.settings.message_path,
|
| 834 |
-
app=sse.handle_post_message,
|
| 835 |
-
)
|
| 836 |
-
)
|
| 837 |
-
# mount these routes last, so they have the lowest route matching precedence
|
| 838 |
-
routes.extend(self._custom_starlette_routes)
|
| 839 |
-
|
| 840 |
-
# Create Starlette app with routes and middleware
|
| 841 |
-
return Starlette(
|
| 842 |
-
debug=self.settings.debug, routes=routes, middleware=middleware
|
| 843 |
)
|
| 844 |
|
| 845 |
def mount(
|
|
|
|
| 16 |
import httpx
|
| 17 |
import pydantic
|
| 18 |
import uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
|
| 20 |
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
| 21 |
from mcp.server.lowlevel.server import LifespanResultT
|
| 22 |
from mcp.server.lowlevel.server import Server as MCPServer
|
|
|
|
| 23 |
from mcp.server.stdio import stdio_server
|
| 24 |
from mcp.types import (
|
| 25 |
AnyFunction,
|
|
|
|
| 35 |
from mcp.types import Tool as MCPTool
|
| 36 |
from pydantic import AnyUrl
|
| 37 |
from starlette.applications import Starlette
|
|
|
|
|
|
|
| 38 |
from starlette.requests import Request
|
| 39 |
from starlette.responses import Response
|
| 40 |
+
from starlette.routing import Route
|
|
|
|
| 41 |
|
| 42 |
import fastmcp.server
|
| 43 |
import fastmcp.settings
|
|
|
|
| 46 |
from fastmcp.prompts.prompt import PromptResult
|
| 47 |
from fastmcp.resources import Resource, ResourceManager
|
| 48 |
from fastmcp.resources.template import ResourceTemplate
|
| 49 |
+
from fastmcp.server.http import RequestContextMiddleware, create_sse_app
|
| 50 |
from fastmcp.tools import ToolManager
|
| 51 |
from fastmcp.tools.tool import Tool
|
| 52 |
from fastmcp.utilities.cache import TimedCache
|
|
|
|
| 146 |
"is specified"
|
| 147 |
)
|
| 148 |
self._auth_server_provider = auth_server_provider
|
| 149 |
+
self._additional_http_routes: list[Route] = []
|
| 150 |
self.dependencies = self.settings.dependencies
|
| 151 |
|
| 152 |
# Set up MCP protocol handlers
|
|
|
|
| 285 |
def decorator(
|
| 286 |
func: Callable[[Request], Awaitable[Response]],
|
| 287 |
) -> Callable[[Request], Awaitable[Response]]:
|
| 288 |
+
self._additional_http_routes.append(
|
| 289 |
Route(
|
| 290 |
path,
|
| 291 |
endpoint=func,
|
|
|
|
| 733 |
|
| 734 |
def sse_app(self) -> Starlette:
|
| 735 |
"""Return an instance of the SSE server app."""
|
| 736 |
+
return create_sse_app(
|
| 737 |
+
server=self,
|
| 738 |
+
message_path=self.settings.message_path,
|
| 739 |
+
sse_path=self.settings.sse_path,
|
| 740 |
+
auth_server_provider=self._auth_server_provider,
|
| 741 |
+
auth_settings=self.settings.auth,
|
| 742 |
+
debug=self.settings.debug,
|
| 743 |
+
additional_routes=self._additional_http_routes,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
)
|
| 745 |
|
| 746 |
def mount(
|