Skip to content
Merged
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Repository Guidelines

## Project Structure & Module Organization
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/`.

## Build, Test, and Development Commands
- `python3 -m pip install -r requirements.txt` prepares runtime dependencies; add `requirements-dev.txt` for local tooling.
- `make format` runs Black and Ruff autofixes across `src/` and `tests/`.
- `make lint` executes Flake8 and Pylint with the repository presets.
- `make test` wraps `pytest` with the configured `tests/` path.
- `make dev` starts Uvicorn on `http://localhost:3000` with hot reload.
- `docker-compose up --build -d` (from `compose.yaml`) builds and runs the API plus MongoDB in containers; use `make clean` to prune stacks.

## Coding Style & Naming Conventions
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.

## Testing Guidelines
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.

## Commit & Pull Request Guidelines
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.

## Security & Configuration Tips
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.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env
- Dev: `python3 -m uvicorn src:app --reload --port 3000`
- Prod: `gunicorn -k uvicorn.workers.UvicornWorker src:app -b 0.0.0.0:3000`

## MCP Server
- The MCP bridge is mounted directly on the FastAPI app and is available at `/mcp` alongside the REST API.
- No extra process is required: `uvicorn src:app` serves both the REST routes and the MCP transport.

## Project structure
```
├── README.md # this file
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
motor
dill
python-dotenv
fastapi
uvloop
pydantic
numpy==1.26.4
pymongo
pymongo>=4.15
jsonpickle
gunicorn
uvicorn
Expand All @@ -16,3 +15,4 @@ opentelemetry.instrumentation.requests
opentelemetry-api
opentelemetry-sdk
tenacity
fastmcp
67 changes: 44 additions & 23 deletions src/api.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
from __future__ import annotations

from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.openapi.utils import get_openapi
from fastapi.responses import RedirectResponse, JSONResponse
from fastapi.responses import JSONResponse, RedirectResponse

from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor

from src import logger, parse_error
from src.routes import flight, environment, motor, rocket
from src.mcp.server import build_mcp
from src.routes import environment, flight, motor, rocket
from src.utils import RocketPyGZipMiddleware

app = FastAPI(

rest_app = FastAPI(
title="Infinity API",
swagger_ui_parameters={
"defaultModelsExpandDepth": 0,
"syntaxHighlight.theme": "obsidian",
}
},
)
app.include_router(flight.router)
app.include_router(environment.router)
app.include_router(motor.router)
app.include_router(rocket.router)

FastAPIInstrumentor.instrument_app(app)
rest_app.include_router(flight.router)
rest_app.include_router(environment.router)
rest_app.include_router(motor.router)
rest_app.include_router(rocket.router)

RequestsInstrumentor().instrument()

# Compress responses above 1KB
app.add_middleware(RocketPyGZipMiddleware, minimum_size=1000)
rest_app.add_middleware(RocketPyGZipMiddleware, minimum_size=1000)


def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
if rest_app.openapi_schema:
return rest_app.openapi_schema
openapi_schema = get_openapi(
title="RocketPy Infinity-API",
version="3.0.0",
Expand All @@ -47,40 +52,56 @@ def custom_openapi():
"<p>Create, manage, and simulate rocket flights, environments, rockets, and motors.</p>"
"<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>"
),
routes=app.routes,
routes=rest_app.routes,
)
openapi_schema["info"]["x-logo"] = {
"url": "https://raw.githubusercontent.com/RocketPy-Team/RocketPy/master/docs/static/RocketPy_Logo_black.png"
}
app.openapi_schema = openapi_schema
return app.openapi_schema
rest_app.openapi_schema = openapi_schema
return rest_app.openapi_schema


app.openapi = custom_openapi
rest_app.openapi = custom_openapi


# Main
@app.get("/", include_in_schema=False)
@rest_app.get("/", include_in_schema=False)
async def main_page():
"""
Redirects to API docs.
"""
"""Redirect to API docs."""
return RedirectResponse(url="/redoc")


# Additional routes
@app.get("/health", status_code=status.HTTP_200_OK, include_in_schema=False)
@rest_app.get(
"/health", status_code=status.HTTP_200_OK, include_in_schema=False
)
async def __perform_healthcheck():
return {"health": "Everything OK!"}


# Global exception handler
@app.exception_handler(RequestValidationError)
@rest_app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
):
exc_str = parse_error(exc)
logger.error(f"{request}: {exc_str}")
logger.error("%s: %s", request, exc_str)
return JSONResponse(
content=exc_str, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
)


# --- MCP server mounted under /mcp -------
mcp_app = build_mcp(rest_app).http_app(path="/")

app = FastAPI(
docs_url=None,
redoc_url=None,
openapi_url=None,
lifespan=mcp_app.lifespan,
)

app.mount("/mcp", mcp_app)
app.mount("/", rest_app)

FastAPIInstrumentor.instrument_app(app)
Empty file added src/mcp/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions src/mcp/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""FastMCP integration helpers for Infinity API."""

from __future__ import annotations

from fastapi import FastAPI
from fastmcp import FastMCP, settings


def build_mcp(app: FastAPI) -> FastMCP:
"""
Create or return a cached FastMCP server that mirrors the given FastAPI app.

Parameters:
app (FastAPI): FastAPI application to mirror; the created FastMCP instance is cached on `app.state.mcp`.

Returns:
FastMCP: The FastMCP instance corresponding to the provided FastAPI app.
"""

if hasattr(app.state, 'mcp'):
return app.state.mcp # type: ignore[attr-defined]

settings.experimental.enable_new_openapi_parser = True
mcp = FastMCP.from_fastapi(app, name=app.title)
app.state.mcp = mcp # type: ignore[attr-defined]
return mcp
Loading