1+ from __future__ import annotations
2+
3+ import logging
4+
5+ from contextlib import asynccontextmanager
16from fastapi import FastAPI , Request , status
27from fastapi .exceptions import RequestValidationError
38from fastapi .openapi .utils import get_openapi
4- from fastapi .responses import RedirectResponse , JSONResponse
9+ from fastapi .responses import JSONResponse , RedirectResponse
510
611from opentelemetry .instrumentation .fastapi import FastAPIInstrumentor
712from opentelemetry .instrumentation .requests import RequestsInstrumentor
813
914from src import logger , parse_error
10- from src .routes import flight , environment , motor , rocket
11- from src .utils import RocketPyGZipMiddleware
1215from src .mcp .server import build_mcp
16+ from src .routes import environment , flight , motor , rocket
17+ from src .utils import RocketPyGZipMiddleware
1318
14- app = FastAPI (
19+ log = logging .getLogger (__name__ )
20+
21+
22+ # --- REST application -------------------------------------------------------
23+
24+ rest_app = FastAPI (
1525 title = "Infinity API" ,
1626 swagger_ui_parameters = {
1727 "defaultModelsExpandDepth" : 0 ,
1828 "syntaxHighlight.theme" : "obsidian" ,
1929 },
2030)
21- app .include_router (flight .router )
22- app .include_router (environment .router )
23- app .include_router (motor .router )
24- app .include_router (rocket .router )
2531
26- _mcp_server = build_mcp (app )
27- app .mount ('/mcp' , _mcp_server .http_app ())
32+ rest_app .include_router (flight .router )
33+ rest_app .include_router (environment .router )
34+ rest_app .include_router (motor .router )
35+ rest_app .include_router (rocket .router )
2836
29- FastAPIInstrumentor .instrument_app (app )
37+ FastAPIInstrumentor .instrument_app (rest_app )
3038RequestsInstrumentor ().instrument ()
3139
3240# Compress responses above 1KB
33- app .add_middleware (RocketPyGZipMiddleware , minimum_size = 1000 )
41+ rest_app .add_middleware (RocketPyGZipMiddleware , minimum_size = 1000 )
3442
3543
3644def custom_openapi ():
37- if app .openapi_schema :
38- return app .openapi_schema
45+ if rest_app .openapi_schema :
46+ return rest_app .openapi_schema
3947 openapi_schema = get_openapi (
4048 title = "RocketPy Infinity-API" ,
4149 version = "3.0.0" ,
@@ -52,40 +60,70 @@ def custom_openapi():
5260 "<p>Create, manage, and simulate rocket flights, environments, rockets, and motors.</p>"
5361 "<p>Please report any bugs at <a href='https://github.com/RocketPy-Team/infinity-api/issues/new/choose' style='text-decoration: none; color: #008CBA;'>GitHub Issues</a></p>"
5462 ),
55- routes = app .routes ,
63+ routes = rest_app .routes ,
5664 )
5765 openapi_schema ["info" ]["x-logo" ] = {
5866 "url" : "https://raw.githubusercontent.com/RocketPy-Team/RocketPy/master/docs/static/RocketPy_Logo_black.png"
5967 }
60- app .openapi_schema = openapi_schema
61- return app .openapi_schema
68+ rest_app .openapi_schema = openapi_schema
69+ return rest_app .openapi_schema
6270
6371
64- app .openapi = custom_openapi
72+ rest_app .openapi = custom_openapi
6573
6674
6775# Main
68- @app .get ("/" , include_in_schema = False )
76+ @rest_app .get ("/" , include_in_schema = False )
6977async def main_page ():
70- """
71- Redirects to API docs.
72- """
78+ """Redirect to API docs."""
7379 return RedirectResponse (url = "/redoc" )
7480
7581
7682# Additional routes
77- @app .get ("/health" , status_code = status .HTTP_200_OK , include_in_schema = False )
83+ @rest_app .get (
84+ "/health" , status_code = status .HTTP_200_OK , include_in_schema = False
85+ )
7886async def __perform_healthcheck ():
7987 return {"health" : "Everything OK!" }
8088
8189
8290# Global exception handler
83- @app .exception_handler (RequestValidationError )
91+ @rest_app .exception_handler (RequestValidationError )
8492async def validation_exception_handler (
8593 request : Request , exc : RequestValidationError
8694):
8795 exc_str = parse_error (exc )
88- logger .error (f" { request } : { exc_str } " )
96+ logger .error ("%s: %s" , request , exc_str )
8997 return JSONResponse (
9098 content = exc_str , status_code = status .HTTP_422_UNPROCESSABLE_ENTITY
9199 )
100+
101+
102+ # --- MCP server mounted under /mcp ------------------------------------------
103+ mcp_app = build_mcp (rest_app ).http_app (path = "/" )
104+
105+
106+ def _combine_lifespans (rest_lifespan , mcp_lifespan ):
107+ """Combine FastAPI and MCP lifespans."""
108+
109+ @asynccontextmanager
110+ async def lifespan (app : FastAPI ):
111+ async with rest_lifespan (app ):
112+ async with mcp_lifespan (app ):
113+ yield
114+
115+ return lifespan
116+
117+
118+ app = FastAPI (
119+ docs_url = None ,
120+ redoc_url = None ,
121+ openapi_url = None ,
122+ lifespan = _combine_lifespans (
123+ rest_app .router .lifespan_context , mcp_app .lifespan
124+ ),
125+ )
126+ app .mount ("/mcp" , mcp_app )
127+ app .mount ("/" , rest_app )
128+
129+ __all__ = ["app" , "rest_app" ]
0 commit comments