ADR-010: MCP Trace Tools Reuse Route Handlers In-Process¶
Status: Accepted
Date: 2026-06-03
Deciders: Heiko Sundermann, AI dev-lead
Context: Studio MCP Trace-Inspection Server (docs/superpowers/specs/2026-06-02-studio-mcp-trace-server-design.md), PR-2
Context and Problem Statement¶
The MCP trace-inspection server exposes the 14 studio-trace capabilities as
native MCP tools. The tools must return data identical to the existing backend
routes (UBER-AC-1 parity) with zero duplicated trace logic (UBER-AC-3). Two
implementation realities surfaced during PR-2 that are non-obvious enough to
record:
- How tools reach the trace logic. Options were: (a) call the existing
FastAPI route handler functions in-process, (b) self-HTTP back to
/api/v1/…, or (c) extract a sharedtrace_inspectionservice the routes and tools both call. - Calling route handlers in-process bypasses FastAPI's
response_model.list_projectsdeclaresresponse_model=ProjectListResponse, which strips the secretgithub_tokenfield on the HTTP path. An in-process call returns the rawProjectORM rows — secret included.
Decision¶
1. Tools call the route handler functions directly, in-process (option a),
with an injected AsyncSession. Tools are thin adapters: arg-map → route call →
result-shape → error-map. They never construct queries or import ORM models
(enforced by tests/mcp/test_no_requery.py, T-AC-9.1). Service extraction
(option c) is explicitly deferred (spec NG1 / R-1) — the web-layer coupling is
accepted and isolated in the adapters; parity tests catch route-signature drift.
2. Adapters whose route returns secret-bearing ORM rows MUST re-project
through the route's published response model before shaping. list_projects_tool
projects through ProjectListResponse(...).model_dump(by_alias=True) so the MCP
output equals the route's published HTTP/CLI body (camelCase, no
github_token). The other 13 tools return route dicts verbatim because their
routes carry no response_model (handler return == HTTP body).
3. The to_structured result-shaping (artifact A4) is a verbatim
pass-through. The spec originally described it as returning a (content, meta)
tuple that lifts tokens_estimated/truncated/hint into a separate MCP
_meta channel. We collapsed it: those keys already live inside the route's
meta dict, which rides along in the verbatim structured content, so a second
channel added complexity without adding access.
Consequences¶
Positive:
- Zero logic duplication; one source of truth for trace shaping (the routes).
- Parity is structurally guaranteed for the 13 verbatim tools.
- The secret-projection rule is a clear, testable invariant
(tests/mcp/test_list_projects_security.py).
Negative / risks:
- Tools are coupled to route-handler signatures (R-1, accepted). Mitigated by the
data-driven parity test (tests/mcp/test_mcp_parity.py).
- Asymmetry: one adapter (list_projects) re-projects while 13 pass through.
Any future tool wrapping a route that returns ORM rows with sensitive fields
MUST follow the §2 rule, or it will leak. This ADR is the durable reminder.
Alternatives Considered¶
- Self-HTTP hop: rejected — extra latency, re-auth, and a network dependency for an in-process capability.
- Service extraction: deferred (NG1) — larger refactor of the fat
observations.py; a future spec may extracttrace_inspection. - Per-tool manual field allow-lists: rejected in favour of reusing the
routes' own
response_model, so the MCP surface tracks the published contract automatically.