CRUMB a card from devarno-cloud

FastMCP Tool Surface (31 tools, HTTP-deployed)

traceo intermediate 6 min read

ELI5

FastMCP is a vending machine of labelled buttons (tools) an AI client can press. In production the machine sits behind the same front door as the web API: an HTTP endpoint with token checking. Some buttons are deliberately disconnected. Every “write a baseline” button answers “not here, that happens upstream”, whoever presses it.

Technical Deep Dive

Source: traceo-cat/traceo_mcp_server/traceo_mcp_server/server.py:261-1700 (tool definitions), :1785-1906 (main, transport wrap), ariel_integration/mcp_tools.py:36-340, ariel_integration/policy.py, services/relationships.py:1-40, auth/rbac.py:394-445; decisions 0104, 0108; traceo-cat origin/main 7d647ab, read 2026-10-25

Transport

Railway runs --transport http, so remote MCP clients reach FastMCP’s streamable-HTTP endpoint through CORSMiddleware(AuthMiddleware(app, allow_anonymous=True)). The initialize handshake works without a token. Tenant-scoped tools then refuse when there is no workspace context. stdio remains the CLI default for local use (MCP_TRANSPORT), but it is not what production serves. A local stdio session has no HTTP layer and therefore no AuthMiddleware.

Catalogue: server.py (21 tools)

FamilyToolsCode guard beyond RLS
Spaceslist_spaces, get_space, create_spaceauthenticated user via SpaceService
Requirementslist_requirements, get_requirement, create_requirement, update_requirement, delete_requirement@require_permission on create/update/delete, a no-op while ENABLE_RBAC is off (traceo-005)
Relationshipscreate_relationship, delete_relationshipprincipal role editor/admin/owner, own-workspace endpoints, DB-enum type (0108)
Query / tracesearch_requirements, trace_requirement, analyze_impact, get_traceability_matrixnone
AIsearch_requirements_semantic (falls back to keyword search), analyze_requirements, ask_documentationnone; optional imports
Versionsget_version_history, compare_requirement_versionsnone
Import / exportimport_requirements (@require_permission, no-op), export_requirementsimport_requirements writes via _scoped_write_services(space_id)

Catalogue: ariel_integration/mcp_tools.py (10 tools)

ToolBehaviour (decision 0104, accepted 2026-10-10)
ariel_baseline_create, ariel_baseline_add_ci, ariel_baseline_freeze, ariel_buildrefused for every caller with BASELINE_WRITE_DISABLED. Baselines are authored upstream in ariel (I-1)
ariel_baseline_verify, ariel_baseline_diff, ariel_baseline_list, ariel_baseline_trace, ariel_validatereads: anonymous callers refused, and the caller’s tenant must be on BASELINE_READ_TENANT_IDS (unset means nobody reads)
ariel_syncadmin/owner only, checked from the principal; never anonymous, never a machine identity

The v1.0 table’s REQUIREMENTS_CREATE / REQUIREMENTS_UPDATE floors on the ariel tools no longer exist. Those tools return a refusal before any service call.

classDiagram
class FastMCP {
+tool() decorator
+http_app()
+run(transport)
}
class ServerTools {
21 tools in server.py
}
class ArielTools {
10 tools in mcp_tools.py
4 write tools always refused
}
class Policy {
baseline_write_refusal()
read_refusal()
sync_refusal()
}
class RelationshipWriteService {
require_relationship_writer()
create()
delete()
}
FastMCP <|-- ServerTools : registers
FastMCP <|-- ArielTools : register_ariel_tools
ArielTools --> Policy : every call
ServerTools --> RelationshipWriteService : create and delete relationship

What actually decides a call

flowchart LR
CL["MCP client"] -->|"HTTPS /mcp"| CORS["CORS"] --> AU["AuthMiddleware allow_anonymous"]
AU --> T{"tool"}
T -->|"ariel write"| REF["BASELINE_WRITE_DISABLED"]
T -->|"relationship write"| PR["principal role check"] --> DB
T -->|"requirement write"| RB["require_permission (no-op, RBAC off)"] --> DB
T -->|"read"| DB[("Postgres as traceo_app, RLS")]

Key Terms

  • @mcp.tool() → the FastMCP decorator that registers an async function as a tool.
  • allow_anonymous → lets the MCP handshake run without a token. Tools that need a workspace refuse on their own.
  • Upstream-only baselines → invariant I-1, enforced by refusing every baseline write in code (0104).
  • _scoped_write_services(space_id) → builds services bound to a validated workspace override for one call.

Q&A

Q: Which decorator registers a Traceo MCP tool? A: @mcp.tool() from fastmcp. server.py registers 21 tools and register_ariel_tools(mcp) adds 10.

Q: Over which transport do remote clients reach the deployed tools? A: Streamable HTTP (--transport http), wrapped in CORS and AuthMiddleware. stdio is only for local runs.

Q: What does ariel_baseline_create do when an owner calls it? A: It returns BASELINE_WRITE_DISABLED. Every baseline write (create, add-CI, freeze, build) is refused for every caller, owner included, by decision 0104.

Q: Which tools write relationships, and what authorizes them? A: create_relationship and delete_relationship. services/relationships.py checks the principal’s role (editor/admin/owner) and keeps both endpoints in the caller’s own workspace, and RLS as traceo_app has the final word. ENABLE_RBAC is not consulted (0108).

Examples

An agent calls create_requirement with a viewer token. With ENABLE_RBAC off, the decorator passes it through. The INSERT then runs as traceo_app with the viewer’s GUCs, and members_create_requirements refuses it, because the "user" row’s role is not owner, admin or editor. The tool returns an error, and the database made the decision.

neighbors on the map