CRUMB a card from devarno-cloud

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:

ModuleTool family
traceo_mcp_server/server.pySpaces, requirements, traceability, RAG, version history
traceo_mcp_server/ariel_integration/mcp_tools.pyAriel baseline lifecycle

Catalogue (server.py — observed)

ToolRBAC
list_spaces / get_space / create_space(create gated by space role)
list_requirements / get_requirementnone
create_requirementREQUIREMENTS_CREATE
update_requirementREQUIREMENTS_UPDATE
delete_requirementREQUIREMENTS_DELETE
search_requirementsnone
trace_requirementnone
analyze_impactnone
get_traceability_matrixnone
search_requirements_semanticnone
analyze_requirementsnone
ask_documentationnone
get_version_history / compare_requirement_versionsnone

Catalogue (ariel_integration/mcp_tools.py — observed)

ToolRBAC
ariel_baseline_createREQUIREMENTS_CREATE
ariel_baseline_add_ciREQUIREMENTS_UPDATE
ariel_baseline_freezeREQUIREMENTS_UPDATE
ariel_baseline_verifynone
ariel_baseline_diffnone
ariel_baseline_listnone
ariel_baseline_tracenone
ariel_buildREQUIREMENTS_UPDATE
ariel_validatenone
ariel_syncREQUIREMENTS_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