FastMCP Tool Surface
traceo intermediate 5 min read
ELI5
FastMCP is a vending machine that holds labelled buttons (tools) Claude can press. Pressing a button still goes through the same back kitchen as the HTTP API — same services, same RBAC — but the button itself lives in server.py, not on a route table.
Technical Deep Dive
Tool Registration
Tools are plain async functions decorated with @mcp.tool() from fastmcp. Two registration loci:
| Module | Tool family |
|---|---|
traceo_mcp_server/server.py | Spaces, requirements, traceability, RAG, version history |
traceo_mcp_server/ariel_integration/mcp_tools.py | Ariel baseline lifecycle |
Catalogue (server.py — observed)
| Tool | RBAC |
|---|---|
list_spaces / get_space / create_space | (create gated by space role) |
list_requirements / get_requirement | none |
create_requirement | REQUIREMENTS_CREATE |
update_requirement | REQUIREMENTS_UPDATE |
delete_requirement | REQUIREMENTS_DELETE |
search_requirements | none |
trace_requirement | none |
analyze_impact | none |
get_traceability_matrix | none |
search_requirements_semantic | none |
analyze_requirements | none |
ask_documentation | none |
get_version_history / compare_requirement_versions | none |
Catalogue (ariel_integration/mcp_tools.py — observed)
| Tool | RBAC |
|---|---|
ariel_baseline_create | REQUIREMENTS_CREATE |
ariel_baseline_add_ci | REQUIREMENTS_UPDATE |
ariel_baseline_freeze | REQUIREMENTS_UPDATE |
ariel_baseline_verify | none |
ariel_baseline_diff | none |
ariel_baseline_list | none |
ariel_baseline_trace | none |
ariel_build | REQUIREMENTS_UPDATE |
ariel_validate | none |
ariel_sync | REQUIREMENTS_UPDATE |
Transport
flowchart LR CLAUDE[Claude / MCP client] -- stdio --> FM[FastMCP runtime] WEB[Next.js client] -- HTTP --> APP[FastAPI app.py] FM --> SVC[Shared services] APP --> SVC SVC --> DB[(Postgres)]Both transports terminate on the same services/ package, so RBAC and validation behave identically regardless of caller.
Why Read-Only Tools Skip @require_permission
Read tools rely on RLS to scope what they can see — a viewer JWT yields a UserContext whose workspace_id is set, which means RLS already restricts results. Adding a permission floor would only block role tiers without enlarging the authorised data set.
Key Terms
@mcp.tool()→ FastMCP decorator that registers an async function as a callable MCP tool.- stdio transport → how Claude Code invokes the MCP server; not exposed over HTTP.
- Ariel tools → baseline management surface, separately registered in
ariel_integration/mcp_tools.py.
Q&A
Q: Can I expose an HTTP-only operation as an MCP tool?
A: Yes — wrap the same service call with @mcp.tool(). Both surfaces share the service layer, so there is no HTTP-only logic to port.
Q: Why does ariel_baseline_freeze require REQUIREMENTS_UPDATE rather than a baseline-specific permission?
A: The current RBAC enum reuses requirement-level permissions for baseline mutation. Splitting baseline permissions out is a future concern; today, anyone allowed to update requirements can also freeze baselines.
Q: Are MCP tools rate-limited?
A: HTTP-bound tool calls go through RateLimitMiddleware; stdio invocations from a local Claude session bypass the middleware entirely because they never traverse the HTTP stack.
Examples
Claude calls search_requirements_semantic(query="auth retry"); FastMCP routes the call to services/rag.py, which embeds the query (AIConfig.embedding_model), runs an HNSW cosine search against requirements.embedding, and returns the top-K rows that survive RLS. The same call over HTTP via a viewer JWT returns the same results — the JWT defines what RLS lets through.
neighbors on the map
- MCP Bridge — 4 Tools (Tier 2) integrating IRIS with Claude or other MCP clients
- CAIRNET MCP Tools for Agent Interaction implementing a new MCP tool for CAIRNET