Problem
The sub-MCPs expose irreversible / high-impact write tools with no machine-readable safety signal and no way to disable them:
delete_secret, rotate_secret, create_secret, update_secret — packages/mcp-vault/src/tools/secret-mutations.ts
delete_vault ("Delete a vault and ALL its secrets. Irreversible.") — packages/mcp-vault/src/tools/vaults.ts
trigger_agent_run (spends FerrFleet run quota / executes agents) — packages/mcp-fleet/src/tools/agents.ts
activate_release (switches the live serving release of a site) — packages/mcp-growth/src/tools/releases.ts
revoke_token — packages/mcp/src/tools/tokens.ts
None of these declare MCP tool annotations. Every server.tool(...) call passes only (name, description, schema, handler) — no annotations: { readOnlyHint, destructiveHint, idempotentHint }. There is also no FERRLABS_MCP_READONLY-style switch to register only read tools.
Why it matters
The danger lives entirely in a free-text description string. MCP clients increasingly use destructiveHint / readOnlyHint to gate auto-approval and to warn users before running a tool. Without them, an assistant can silently delete_secret or trigger_agent_run (which costs money and runs code) with no client-side guardrail. A read-only mode is the standard mitigation for users who want the MCP for inspection only.
Proposed approach
- Add an
annotations object to every server.tool registration: readOnlyHint: true for list/get/fetch tools, destructiveHint: true (and idempotentHint where applicable) for the mutating tools above.
- Add a
FERRLABS_MCP_READONLY=1 env flag that skips registration of all write/destructive tools, documented in the README.
- Centralize the registration helper so the annotation is mandatory at the type level (hard to forget when adding a new tool).
Acceptance criteria
tools/list reports correct readOnlyHint / destructiveHint for every tool.
- With
FERRLABS_MCP_READONLY=1, no destructive tool is registered; a test asserts the registered set.
- README documents the read-only flag and the annotation convention.
Problem
The sub-MCPs expose irreversible / high-impact write tools with no machine-readable safety signal and no way to disable them:
delete_secret,rotate_secret,create_secret,update_secret—packages/mcp-vault/src/tools/secret-mutations.tsdelete_vault("Delete a vault and ALL its secrets. Irreversible.") —packages/mcp-vault/src/tools/vaults.tstrigger_agent_run(spends FerrFleet run quota / executes agents) —packages/mcp-fleet/src/tools/agents.tsactivate_release(switches the live serving release of a site) —packages/mcp-growth/src/tools/releases.tsrevoke_token—packages/mcp/src/tools/tokens.tsNone of these declare MCP tool annotations. Every
server.tool(...)call passes only(name, description, schema, handler)— noannotations: { readOnlyHint, destructiveHint, idempotentHint }. There is also noFERRLABS_MCP_READONLY-style switch to register only read tools.Why it matters
The danger lives entirely in a free-text description string. MCP clients increasingly use
destructiveHint/readOnlyHintto gate auto-approval and to warn users before running a tool. Without them, an assistant can silentlydelete_secretortrigger_agent_run(which costs money and runs code) with no client-side guardrail. A read-only mode is the standard mitigation for users who want the MCP for inspection only.Proposed approach
annotationsobject to everyserver.toolregistration:readOnlyHint: truefor list/get/fetch tools,destructiveHint: true(andidempotentHintwhere applicable) for the mutating tools above.FERRLABS_MCP_READONLY=1env flag that skips registration of all write/destructive tools, documented in the README.Acceptance criteria
tools/listreports correctreadOnlyHint/destructiveHintfor every tool.FERRLABS_MCP_READONLY=1, no destructive tool is registered; a test asserts the registered set.