Spaces:
Running
Running
Merge pull request #558 from jlowin/custom-routes
Browse files
src/fastmcp/server/http.py
CHANGED
|
@@ -241,6 +241,7 @@ def create_sse_app(
|
|
| 241 |
# Add custom routes with lowest precedence
|
| 242 |
if routes:
|
| 243 |
server_routes.extend(routes)
|
|
|
|
| 244 |
|
| 245 |
# Add middleware
|
| 246 |
if middleware:
|
|
@@ -359,6 +360,7 @@ def create_streamable_http_app(
|
|
| 359 |
# Add custom routes with lowest precedence
|
| 360 |
if routes:
|
| 361 |
server_routes.extend(routes)
|
|
|
|
| 362 |
|
| 363 |
# Add middleware
|
| 364 |
if middleware:
|
|
|
|
| 241 |
# Add custom routes with lowest precedence
|
| 242 |
if routes:
|
| 243 |
server_routes.extend(routes)
|
| 244 |
+
server_routes.extend(server._additional_http_routes)
|
| 245 |
|
| 246 |
# Add middleware
|
| 247 |
if middleware:
|
|
|
|
| 360 |
# Add custom routes with lowest precedence
|
| 361 |
if routes:
|
| 362 |
server_routes.extend(routes)
|
| 363 |
+
server_routes.extend(server._additional_http_routes)
|
| 364 |
|
| 365 |
# Add middleware
|
| 366 |
if middleware:
|
src/fastmcp/server/server.py
CHANGED
|
@@ -854,7 +854,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 854 |
auth_server_provider=self._auth_server_provider,
|
| 855 |
auth_settings=self.settings.auth,
|
| 856 |
debug=self.settings.debug,
|
| 857 |
-
routes=self._additional_http_routes,
|
| 858 |
middleware=middleware,
|
| 859 |
)
|
| 860 |
|
|
@@ -905,7 +904,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 905 |
json_response=self.settings.json_response,
|
| 906 |
stateless_http=self.settings.stateless_http,
|
| 907 |
debug=self.settings.debug,
|
| 908 |
-
routes=self._additional_http_routes,
|
| 909 |
middleware=middleware,
|
| 910 |
)
|
| 911 |
elif transport == "sse":
|
|
@@ -916,7 +914,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 916 |
auth_server_provider=self._auth_server_provider,
|
| 917 |
auth_settings=self.settings.auth,
|
| 918 |
debug=self.settings.debug,
|
| 919 |
-
routes=self._additional_http_routes,
|
| 920 |
middleware=middleware,
|
| 921 |
)
|
| 922 |
|
|
|
|
| 854 |
auth_server_provider=self._auth_server_provider,
|
| 855 |
auth_settings=self.settings.auth,
|
| 856 |
debug=self.settings.debug,
|
|
|
|
| 857 |
middleware=middleware,
|
| 858 |
)
|
| 859 |
|
|
|
|
| 904 |
json_response=self.settings.json_response,
|
| 905 |
stateless_http=self.settings.stateless_http,
|
| 906 |
debug=self.settings.debug,
|
|
|
|
| 907 |
middleware=middleware,
|
| 908 |
)
|
| 909 |
elif transport == "sse":
|
|
|
|
| 914 |
auth_server_provider=self._auth_server_provider,
|
| 915 |
auth_settings=self.settings.auth,
|
| 916 |
debug=self.settings.debug,
|
|
|
|
| 917 |
middleware=middleware,
|
| 918 |
)
|
| 919 |
|
tests/server/http/test_custom_routes.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from starlette.requests import Request
|
| 3 |
+
from starlette.responses import JSONResponse
|
| 4 |
+
from starlette.routing import Route
|
| 5 |
+
|
| 6 |
+
from fastmcp import FastMCP
|
| 7 |
+
from fastmcp.server.http import create_sse_app, create_streamable_http_app
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TestCustomRoutes:
|
| 11 |
+
@pytest.fixture
|
| 12 |
+
def server_with_custom_route(self):
|
| 13 |
+
"""Create a FastMCP server with a custom route."""
|
| 14 |
+
server = FastMCP()
|
| 15 |
+
|
| 16 |
+
@server.custom_route("/custom-route", methods=["GET"])
|
| 17 |
+
async def custom_route(request: Request):
|
| 18 |
+
return JSONResponse({"message": "custom route"})
|
| 19 |
+
|
| 20 |
+
return server
|
| 21 |
+
|
| 22 |
+
def test_custom_routes_via_server_http_app(self, server_with_custom_route):
|
| 23 |
+
"""Test that custom routes are included when using server.http_app()."""
|
| 24 |
+
# Get the app via server.http_app()
|
| 25 |
+
app = server_with_custom_route.http_app()
|
| 26 |
+
|
| 27 |
+
# Verify that the custom route is included
|
| 28 |
+
custom_route_found = False
|
| 29 |
+
for route in app.routes:
|
| 30 |
+
if isinstance(route, Route) and route.path == "/custom-route":
|
| 31 |
+
custom_route_found = True
|
| 32 |
+
break
|
| 33 |
+
|
| 34 |
+
assert custom_route_found, "Custom route was not found in app routes"
|
| 35 |
+
|
| 36 |
+
def test_custom_routes_via_streamable_http_app_direct(
|
| 37 |
+
self, server_with_custom_route
|
| 38 |
+
):
|
| 39 |
+
"""Test that custom routes are included when using create_streamable_http_app directly."""
|
| 40 |
+
# Create the app by calling the constructor function directly
|
| 41 |
+
app = create_streamable_http_app(
|
| 42 |
+
server=server_with_custom_route, streamable_http_path="/api"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Verify that the custom route is included
|
| 46 |
+
custom_route_found = False
|
| 47 |
+
for route in app.routes:
|
| 48 |
+
if isinstance(route, Route) and route.path == "/custom-route":
|
| 49 |
+
custom_route_found = True
|
| 50 |
+
break
|
| 51 |
+
|
| 52 |
+
assert custom_route_found, "Custom route was not found in app routes"
|
| 53 |
+
|
| 54 |
+
def test_custom_routes_via_sse_app_direct(self, server_with_custom_route):
|
| 55 |
+
"""Test that custom routes are included when using create_sse_app directly."""
|
| 56 |
+
# Create the app by calling the constructor function directly
|
| 57 |
+
app = create_sse_app(
|
| 58 |
+
server=server_with_custom_route, message_path="/message", sse_path="/sse"
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Verify that the custom route is included
|
| 62 |
+
custom_route_found = False
|
| 63 |
+
for route in app.routes:
|
| 64 |
+
if isinstance(route, Route) and route.path == "/custom-route":
|
| 65 |
+
custom_route_found = True
|
| 66 |
+
break
|
| 67 |
+
|
| 68 |
+
assert custom_route_found, "Custom route was not found in app routes"
|
| 69 |
+
|
| 70 |
+
def test_multiple_custom_routes(
|
| 71 |
+
self,
|
| 72 |
+
):
|
| 73 |
+
"""Test that multiple custom routes are included in both methods."""
|
| 74 |
+
server = FastMCP()
|
| 75 |
+
|
| 76 |
+
custom_paths = ["/route1", "/route2", "/route3"]
|
| 77 |
+
|
| 78 |
+
# Add multiple custom routes
|
| 79 |
+
for path in custom_paths:
|
| 80 |
+
|
| 81 |
+
@server.custom_route(path, methods=["GET"])
|
| 82 |
+
async def custom_route(request: Request):
|
| 83 |
+
return JSONResponse({"message": f"route {path}"})
|
| 84 |
+
|
| 85 |
+
# Test with server.http_app()
|
| 86 |
+
app1 = server.http_app()
|
| 87 |
+
|
| 88 |
+
# Test with direct constructor call
|
| 89 |
+
app2 = create_streamable_http_app(server=server, streamable_http_path="/api")
|
| 90 |
+
|
| 91 |
+
# Check all routes are in both apps
|
| 92 |
+
for path in custom_paths:
|
| 93 |
+
# Check in app1
|
| 94 |
+
route_in_app1 = any(
|
| 95 |
+
isinstance(route, Route) and route.path == path for route in app1.routes
|
| 96 |
+
)
|
| 97 |
+
assert route_in_app1, f"Route {path} not found in server.http_app()"
|
| 98 |
+
|
| 99 |
+
# Check in app2
|
| 100 |
+
route_in_app2 = any(
|
| 101 |
+
isinstance(route, Route) and route.path == path for route in app2.routes
|
| 102 |
+
)
|
| 103 |
+
assert route_in_app2, (
|
| 104 |
+
f"Route {path} not found in create_streamable_http_app()"
|
| 105 |
+
)
|
tests/server/{test_http_dependencies.py → http/test_http_dependencies.py}
RENAMED
|
File without changes
|
tests/server/{test_http_middleware.py → http/test_http_middleware.py}
RENAMED
|
File without changes
|