Skip to content

Commit c7bb348

Browse files
MCP server instrumentation and repositories interface cleaning (#61)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 7e4c8c2 commit c7bb348

File tree

8 files changed

+275
-92
lines changed

8 files changed

+275
-92
lines changed

AGENTS.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
Source code lives in `src/`, with FastAPI entrypoints in `src/api.py` and domain layers grouped by concern (`controllers/`, `services/`, `repositories/`, `models/`, `routes/`, `views/`). Shared settings and helpers sit in `src/settings/` and `src/utils.py`. Tests mirror this layout under `tests/unit/` and `tests/integration/` for focused and end-to-end coverage. Deployment assets stay at the root (`Dockerfile`, `compose.yaml`, `Makefile`), while virtualenv tooling is kept in `infinity_env/`.
5+
6+
## Build, Test, and Development Commands
7+
- `python3 -m pip install -r requirements.txt` prepares runtime dependencies; add `requirements-dev.txt` for local tooling.
8+
- `make format` runs Black and Ruff autofixes across `src/` and `tests/`.
9+
- `make lint` executes Flake8 and Pylint with the repository presets.
10+
- `make test` wraps `pytest` with the configured `tests/` path.
11+
- `make dev` starts Uvicorn on `http://localhost:3000` with hot reload.
12+
- `docker-compose up --build -d` (from `compose.yaml`) builds and runs the API plus MongoDB in containers; use `make clean` to prune stacks.
13+
14+
## Coding Style & Naming Conventions
15+
Target Python 3.12 and a 79-character line length (Black, Ruff, and Pylint enforce this). Prefer module-level functions in `snake_case`, classes in `PascalCase`, and constants in `UPPER_SNAKE_CASE`. Keep FastAPI routers named `<feature>_router` inside `src/routes/`, and align Pydantic models with `CamelCase` class names under `src/models/`. Run `make format` before opening a PR to avoid stylistic churn.
16+
17+
## Testing Guidelines
18+
Pytest drives both unit and integration suites; name files `test_<feature>.py` and group fixtures near usage. Place pure-function tests in `tests/unit/` and API or database flows in `tests/integration/`. Execute `pytest tests/integration/test_environment.py -k simulate` to target scenarios while keeping `--import-mode=importlib` behavior intact. New features should include happy-path and failure-case coverage, plus integration smoke tests when touching MongoDB writes.
19+
20+
## Commit & Pull Request Guidelines
21+
Git history favors concise, uppercase prefixes (`BUG:`, `ENH:`, `MNT:`) followed by a short imperative summary and optional issue reference, e.g. `ENH: streamline rocket encoders (#58)`. Squash commits that fix review feedback before merging. Pull requests should describe intent, list API or schema changes, link to tracking issues, and attach screenshots or sample responses when observable behavior shifts.
22+
23+
## Security & Configuration Tips
24+
Never commit `.env` or credentials; instead, document required keys such as `MONGODB_CONNECTION_STRING` in the PR. Use `src/secrets.py` helpers for secret access rather than inlining values, and prefer Docker secrets or environment variables when deploying.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env
3030
- Dev: `python3 -m uvicorn src:app --reload --port 3000`
3131
- Prod: `gunicorn -k uvicorn.workers.UvicornWorker src:app -b 0.0.0.0:3000`
3232

33+
## MCP Server
34+
- The MCP bridge is mounted directly on the FastAPI app and is available at `/mcp` alongside the REST API.
35+
- No extra process is required: `uvicorn src:app` serves both the REST routes and the MCP transport.
36+
3337
## Project structure
3438
```
3539
├── README.md # this file

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
motor
21
dill
32
python-dotenv
43
fastapi
54
uvloop
65
pydantic
76
numpy==1.26.4
8-
pymongo
7+
pymongo>=4.15
98
jsonpickle
109
gunicorn
1110
uvicorn
@@ -16,3 +15,4 @@ opentelemetry.instrumentation.requests
1615
opentelemetry-api
1716
opentelemetry-sdk
1817
tenacity
18+
fastmcp

src/api.py

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,41 @@
1+
from __future__ import annotations
2+
13
from fastapi import FastAPI, Request, status
24
from fastapi.exceptions import RequestValidationError
35
from fastapi.openapi.utils import get_openapi
4-
from fastapi.responses import RedirectResponse, JSONResponse
6+
from fastapi.responses import JSONResponse, RedirectResponse
57

68
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
79
from opentelemetry.instrumentation.requests import RequestsInstrumentor
810

911
from src import logger, parse_error
10-
from src.routes import flight, environment, motor, rocket
12+
from src.mcp.server import build_mcp
13+
from src.routes import environment, flight, motor, rocket
1114
from src.utils import RocketPyGZipMiddleware
1215

13-
app = FastAPI(
16+
17+
rest_app = FastAPI(
18+
title="Infinity API",
1419
swagger_ui_parameters={
1520
"defaultModelsExpandDepth": 0,
1621
"syntaxHighlight.theme": "obsidian",
17-
}
22+
},
1823
)
19-
app.include_router(flight.router)
20-
app.include_router(environment.router)
21-
app.include_router(motor.router)
22-
app.include_router(rocket.router)
2324

24-
FastAPIInstrumentor.instrument_app(app)
25+
rest_app.include_router(flight.router)
26+
rest_app.include_router(environment.router)
27+
rest_app.include_router(motor.router)
28+
rest_app.include_router(rocket.router)
29+
2530
RequestsInstrumentor().instrument()
2631

2732
# Compress responses above 1KB
28-
app.add_middleware(RocketPyGZipMiddleware, minimum_size=1000)
33+
rest_app.add_middleware(RocketPyGZipMiddleware, minimum_size=1000)
2934

3035

3136
def custom_openapi():
32-
if app.openapi_schema:
33-
return app.openapi_schema
37+
if rest_app.openapi_schema:
38+
return rest_app.openapi_schema
3439
openapi_schema = get_openapi(
3540
title="RocketPy Infinity-API",
3641
version="3.0.0",
@@ -47,40 +52,56 @@ def custom_openapi():
4752
"<p>Create, manage, and simulate rocket flights, environments, rockets, and motors.</p>"
4853
"<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>"
4954
),
50-
routes=app.routes,
55+
routes=rest_app.routes,
5156
)
5257
openapi_schema["info"]["x-logo"] = {
5358
"url": "https://raw.githubusercontent.com/RocketPy-Team/RocketPy/master/docs/static/RocketPy_Logo_black.png"
5459
}
55-
app.openapi_schema = openapi_schema
56-
return app.openapi_schema
60+
rest_app.openapi_schema = openapi_schema
61+
return rest_app.openapi_schema
5762

5863

59-
app.openapi = custom_openapi
64+
rest_app.openapi = custom_openapi
6065

6166

6267
# Main
63-
@app.get("/", include_in_schema=False)
68+
@rest_app.get("/", include_in_schema=False)
6469
async def main_page():
65-
"""
66-
Redirects to API docs.
67-
"""
70+
"""Redirect to API docs."""
6871
return RedirectResponse(url="/redoc")
6972

7073

7174
# Additional routes
72-
@app.get("/health", status_code=status.HTTP_200_OK, include_in_schema=False)
75+
@rest_app.get(
76+
"/health", status_code=status.HTTP_200_OK, include_in_schema=False
77+
)
7378
async def __perform_healthcheck():
7479
return {"health": "Everything OK!"}
7580

7681

7782
# Global exception handler
78-
@app.exception_handler(RequestValidationError)
83+
@rest_app.exception_handler(RequestValidationError)
7984
async def validation_exception_handler(
8085
request: Request, exc: RequestValidationError
8186
):
8287
exc_str = parse_error(exc)
83-
logger.error(f"{request}: {exc_str}")
88+
logger.error("%s: %s", request, exc_str)
8489
return JSONResponse(
8590
content=exc_str, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
8691
)
92+
93+
94+
# --- MCP server mounted under /mcp -------
95+
mcp_app = build_mcp(rest_app).http_app(path="/")
96+
97+
app = FastAPI(
98+
docs_url=None,
99+
redoc_url=None,
100+
openapi_url=None,
101+
lifespan=mcp_app.lifespan,
102+
)
103+
104+
app.mount("/mcp", mcp_app)
105+
app.mount("/", rest_app)
106+
107+
FastAPIInstrumentor.instrument_app(app)

src/mcp/__init__.py

Whitespace-only changes.

src/mcp/server.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""FastMCP integration helpers for Infinity API."""
2+
3+
from __future__ import annotations
4+
5+
from fastapi import FastAPI
6+
from fastmcp import FastMCP, settings
7+
8+
9+
def build_mcp(app: FastAPI) -> FastMCP:
10+
"""
11+
Create or return a cached FastMCP server that mirrors the given FastAPI app.
12+
13+
Parameters:
14+
app (FastAPI): FastAPI application to mirror; the created FastMCP instance is cached on `app.state.mcp`.
15+
16+
Returns:
17+
FastMCP: The FastMCP instance corresponding to the provided FastAPI app.
18+
"""
19+
20+
if hasattr(app.state, 'mcp'):
21+
return app.state.mcp # type: ignore[attr-defined]
22+
23+
settings.experimental.enable_new_openapi_parser = True
24+
mcp = FastMCP.from_fastapi(app, name=app.title)
25+
app.state.mcp = mcp # type: ignore[attr-defined]
26+
return mcp

0 commit comments

Comments
 (0)