CRUMB a card from devarno-cloud

Two-Service MCP & Engine Architecture (as deployed)

traceo beginner 6 min read

ELI5

Traceo has two Python counters and two web shopfronts. The MCP counter (:8000) handles requirements, traceability, baselines and audit, for both Claude-style MCP clients and the web app. The engine counter (:8001) takes CSV uploads as background jobs. The web app at app.traceo.cat and the landing site are Next.js on Vercel. There is a trap in the MCP server: a file called app.py looks like the server, but production never runs it.

Technical Deep Dive

Source: traceo-cat/traceo_mcp_server/railway.toml:11, traceo_mcp_server/traceo_mcp_server/server.py:88-160 and :1785-1906, engine/railway.toml:14-20, engine/src/engine/server.py:133-168, web/apps/{client,landing}/vercel.json, git log a810d5d (2026-07-11); traceo-cat origin/main 7d647ab; live GET /health on mcp.traceo.cat and engine.traceo.cat, read 2026-10-25

Service split

ServicePathPortDeployed start commandHost
MCP servertraceo_mcp_server/8000 (PORT/MCP_PORT)python -m traceo_mcp_server.server --transport http (railway.toml:11, same as the Dockerfile CMD)Railway
Engineengine/src/engine/8001python -m engine.server (engine/railway.toml:20), built with DISABLE_RUST=1Railway
Web clientweb/apps/client—Next 14 next buildVercel, app.traceo.cat
Landingweb/apps/landing—Next 14 next buildVercel

Deploys are native git integrations: Railway and Vercel build from main themselves. The GitHub deploy-web / deploy-backend workflows were deleted on 2026-07-11 (a810d5d, #195). Vercel’s ignored-build step is scripts/vercel-ignore.sh <surface>, which replaced a turbo-ignore string that could never skip anything (the repo has no turbo.json).

The live engine /health on 2026-10-25 reported rust_backend: fallback, consistent with the DISABLE_RUST=1 build arg: production ingestion runs the pure-Python path.

server.py is the server; app.py is not

server.py builds its own HTTP surface. At import time _mount_http_routers() creates one combined FastAPI app, includes seven routers from routes/ (health, dashboard, requirements, relationships, spaces, ariel, audit), registers the shared exception handlers, adds AuthMiddleware (probe paths excluded), and mounts it at "" on FastMCP’s _additional_http_routes. main() then wraps FastMCP’s http_app as CORS (outermost, default origins https://app.traceo.cat,http://localhost:3000) around AuthMiddleware(allow_anonymous=True) and calls mcp.run(transport="http").

app.py is a separate FastAPI assembly that adds MetricsMiddleware, SentryContextMiddleware, LoggingMiddleware and the per-IP rate limiter. Nothing on the deployed path imports it; only tests do. A route or middleware added only to app.py does not exist in production. routes/health.py says this in its docstring: /metrics was 404 on the deployed host until it moved onto a router (see traceo-011, traceo-020).

flowchart LR
subgraph clients[Clients]
MCPC["MCP client (HTTP /mcp, or stdio locally)"]
WEB["Next.js client app.traceo.cat"]
end
subgraph mcp["MCP server :8000 (Railway)"]
CORS["CORSMiddleware"] --> AUTH["AuthMiddleware allow_anonymous"]
AUTH --> FM["FastMCP tools"]
AUTH --> COMB["combined FastAPI app: 7 routers + AuthMiddleware"]
APPPY["app.py (not deployed)"]
end
subgraph eng["Engine :8001 (Railway)"]
RL["RateLimit 50/min"] --> EAUTH["AuthMiddleware JWKS"] --> JOBS["/jobs JobManager in memory"]
end
PG[("Postgres + pgvector, RLS")]
MCPC --> CORS
WEB --> CORS
FM --> PG
COMB --> PG
WEB -.->|"engineClient (no live caller of /jobs/ingest found)"| RL

Engine middleware order

FastAPI runs the last-added middleware first. The engine adds CORS, Metrics, Sentry context, Logging, Auth, then the rate limiter (server.py:142-168), so at runtime the order is rate limiter → Auth → Logging → Sentry → Metrics → CORS → handler. An unauthenticated request still spends rate-limit budget; the live engine returns x-ratelimit-* headers on a 401.

No inter-service call

There is no webhook between the services. A positive search on 2026-10-25 for webhook, httpx.post and requests.post under engine/src/engine/ found nothing, and the MCP server has no routes/webhooks.py. The v1.0 card’s “engine notifies MCP via webhook” was false.

sequenceDiagram
participant R as Railway
participant S as server.py
participant F as FastMCP
R->>S: python -m traceo_mcp_server.server --transport http
S->>S: import-time _mount_http_routers (7 routers, AuthMiddleware)
S->>F: wrap http_app as CORS around Auth(allow_anonymous)
S->>F: mcp.run(transport http)
F-->>R: serves /health, /metrics, /api/*, /mcp

Key Terms

  • Combined app → the single FastAPI app server.py builds from routes/ and mounts on FastMCP. It is the only HTTP surface production serves.
  • app.py → an alternative assembly that tests import. Not deployed.
  • Native git deploy → Railway and Vercel build on push to main. No GitHub Actions deploy job has existed since 2026-07-11.
  • allow_anonymous → the MCP handshake (initialize) succeeds without a token, and tenant-scoped tools refuse on their own when there is no workspace context.

Q&A

Q: Which command does Railway run to start the MCP server, and which file does that command never import? A: python -m traceo_mcp_server.server --transport http (traceo_mcp_server/railway.toml:11). It never imports app.py, so app.py’s metrics, logging and rate-limit middleware are absent in production.

Q: Which port does the ingestion engine listen on? A: 8001 (EXPOSE 8001 and a /health curl in engine/Dockerfile). Railway starts it with python -m engine.server.

Q: How do the Next.js surfaces and the two Python services get deployed? A: Through native git integrations. Vercel builds web/apps/client and web/apps/landing, gated by scripts/vercel-ignore.sh. Railway builds the two Dockerfiles. The GitHub deploy workflows were deleted on 2026-07-11, and the only deploy check left in GitHub is the observer in traceo-018.

Q: Where is the bearer token checked on the MCP server’s HTTP surface? A: In AuthMiddleware, twice over. It is registered on the combined router app (server.py:137) and wrapped around FastMCP’s whole HTTP app with allow_anonymous=True (server.py:1883). @require_permission decorators exist but are no-ops unless ENABLE_RBAC is on (traceo-005).

Examples

You add GET /api/requirements/stats in app.py. CI goes green because a test imports app.py. Production 404s, because Railway runs server.py and the combined app never includes that route. Put the route on a router in routes/ and add the router to the list in _mount_http_routers().

neighbors on the map