#Ratifia MCP (for agents)
Give an AI agent a human-in-the-loop. The Ratifia MCP server lets an agent (Claude Code, Claude Desktop, Cursor, or anything that speaks MCP) pause on a human decision mid-task and resume once it's answered. The agent is the "engine"; Ratifia owns the decision, the notification, and the verdict.
Use it when the agent is about to do something consequential (deploy, delete, spend, send an email externally), needs a human to pick between options, or wants a person to review and refine a draft before it goes out.
#Quick start
Fastest path: let your AI assistant wire it up. Copy the block below and paste it to your coding agent: Claude Code, Cursor, Claude Desktop, Windsurf, or any MCP client. It carries everything the assistant needs; it will ask you for your API key rather than guess it.
Install the Ratifia MCP server into my MCP client. Ratifia is a human-in-the-loop MCP: it lets you (the agent) pause on a humandecision mid-task and resume once it's answered. Add it as a stdio server. Server to add: name: ratifia command: npx args: ["-y", "@ratifia/mcp"] env: RATIFIA_API_KEY (required): my org-scoped worker API key RATIFIA_APPROVER_EMAIL (optional): email decisions are assigned to Steps:1. Ask me for RATIFIA_API_KEY and RATIFIA_APPROVER_EMAIL. Never invent them. (I get the key from the Ratifia dashboard -> API Keys; it's shown once at creation.)2. Detect my client and edit the RIGHT config: - Claude Code -> run: claude mcp add ratifia -e RATIFIA_API_KEY=<key> \ -e RATIFIA_APPROVER_EMAIL=<email> -- npx -y @ratifia/mcp - Claude Desktop -> edit claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json), add the server under "mcpServers". - Cursor -> edit ~/.cursor/mcp.json (or .cursor/mcp.json in-project), under "mcpServers". - Other client -> add the equivalent stdio server entry.3. Use ONLY the fields above. Add no other env vars.4. Tell me to restart the client, then confirm these tools are available: request_decision, await_decision, check_decision, list_pending_decisions, cancel_decision. If anything is unclear, ask me instead of guessing.
Prefer to do it by hand? The full config and options are in Configure it below.
#Configure it
The MCP runs as a local process your client launches over stdio. Add it to your MCP client's config with one required env var, your Ratifia API key:
{ "mcpServers": { "ratifia": { "command": "npx", "args": ["-y", "@ratifia/mcp"], "env": { "RATIFIA_API_KEY": "your-org-worker-api-key", "RATIFIA_APPROVER_EMAIL": "you@example.com" } } }}
That's it. Restart your client and the agent gains the Ratifia tools.
Where the API key comes from
RATIFIA_API_KEY is an org-scoped worker API key. Create one in the Ratifia dashboard under API Keys. It's shown once at creation (store it somewhere safe; it's hashed afterward and can't be retrieved). It scopes every decision the agent opens to your org.
#Environment variables
| Variable | Required | Default | Purpose |
|---|---|---|---|
RATIFIA_API_KEY | ✅ | none | Org-scoped worker API key (from the dashboard). |
RATIFIA_API_URL | https://api.ratifia.com | API base; point at a different environment. | |
RATIFIA_APP_URL | https://app.ratifia.com | Dashboard base for the decision links returned to the agent. | |
RATIFIA_APPROVER_EMAIL | none | Who the decision is assigned to / notified. | |
RATIFIA_POLICY | none | The key of a policy deciding who reviews the agent's decisions and the quorum they need. It must exist: an unknown key is a tool error, and the gate hook denies. | |
RATIFIA_WORKFLOW | ratifia-mcp (ratifia-gate for the hook) | The workflow decisions are filed under: the project or effort the dashboard's Workflow filter groups by. A request_decision call can override it with workflow. (0.9.0+) |
Who is asked, and on which surface, is decided inside Ratifia by the org's policy; see how the approver is reached. The MCP is a thin request/verdict bridge.
#Multiple environments
Point separate registrations at different environments by giving each its own URL + key, e.g. a ratifia-stg and a ratifia-prod, each with its own RATIFIA_API_URL and RATIFIA_API_KEY.
#The tools
| Tool | What it does |
|---|---|
request_decision | Open a decision, notify the human, return a decision_id. |
await_decision | Block until the verdict lands (or a max wait elapses). |
check_decision | Poll a decision's status once. |
list_pending_decisions | The agent's inbox: what's outstanding. |
cancel_decision | Withdraw a decision the agent no longer needs. |
attach_evidence | Attach material the decision is judged on (a Slack thread, an email) while it's pending. |
#Filing decisions: workflow and step
Every decision carries two labels, and the dashboard's Workflow filter groups by the first:
workflow: the project or ongoing effort, e.g. the repo name orbilling-refunds. Reuse the exact same string for related decisions, and keep ids, versions and environments out of it.step: what's specific to this decision, e.g.deploy-v2.3-prodorrefund-INV-1042. Defaults to the title.
Without them every agent decision lands in one ratifia-mcp group. Set a default per server with RATIFIA_WORKFLOW, or pass workflow / step to request_decision (0.9.0+).
Typical loop: the agent calls request_decision before the risky step, waits for the verdict, and proceeds only on approved. On rejected/expired it adapts.
How it waits matters. await_decision blocks the turn, which is right for a long-running agent and wrong for one that runs in short cycles — a cycle that ends while a decision is open will never see the verdict unless something reads it back. See FAQ: my agent never came back for the verdict.
#Decision shapes
request_decision isn't only yes/no. Shape the answer with response_type:
response_type | The human… | The agent reads |
|---|---|---|
approval (default) | approves or rejects | verdict (+ note) |
select | picks from options you supply (set allow_multiple for many) | response_value.selected (ids → labels via response_spec.options) |
text | types a value | response_value.text |
request_decision( title: "Which environment?", question: "Where should I deploy?", response_type: "select", options: [{ label: "Staging" }, { label: "Production" }])
#Refining a draft: Conversational Approvals
Pass proposed_output to attach an editable draft (e.g. an email). The human refines it in conversation, either in the dashboard or by replying in the Slack thread, and the agent gets their final version back on output.content (with a refined flag). Act on output.content, not the draft you sent. See Conversational Approvals.
request_decision( title: "Send welcome email", question: "Review this before I send it.", proposed_output: { title: "Welcome email", content: "<your draft>" })
#Teach your agent when to ask: a skill template
The MCP gives an agent the tools. A skill tells it when to reach for them, and how to file what it asks, so decisions show up consistently instead of whenever the model happens to think of it. For Claude Code, save this as .claude/skills/ratifia-decision/SKILL.md in a repo (shared with the team) or ~/.claude/skills/ratifia-decision/SKILL.md (just you), then adjust the lists to your own risk lines.
---name: ratifia-decisiondescription: Ask a human for a decision via Ratifia before a consequential or irreversible action, or when a requirement is ambiguous and guessing wrong is costly, instead of guessing or stalling. Use before production deploys, deletes/drops, data migrations, spending, sending anything externally, or credential changes. Requires the Ratifia MCP tools (request_decision, await_decision).--- # Asking a human via Ratifia When you need a person's approval or input and should not assume it, create a Ratifiadecision. They can answer from anywhere, and you continue on their answer. ## When to ask Ask before:- Production deploys, deletes/drops, data migrations- Spending money, or sending email/messages to people outside the team- Rotating or changing credentials- A real fork where the person's preference changes the outcome Don't ask for trivial, reversible or already-authorized steps. That's friction, not safety. ## How 1. Call `request_decision` with: - `title`: short, e.g. "Deploy API to prod?" - `question`: exactly what you need decided - `context`: the command, diff or options and their tradeoffs, so they can decide without a back-and-forth - `workflow`: the project this belongs to (see Filing) - `step`: what is specific to this decision - `timeout_minutes`: only if it's time-sensitive2. Wait with `await_decision`. The person may be away from their keyboard; that's expected.3. Only act on `approved`. The `note` is their actual answer: read it and follow it. On `rejected` or `expired`, don't proceed: adapt, or ask again with more context. Prefer one well-scoped decision with rich context over several small ones. ## Filing: workflow and step - `workflow` = the project or effort: the repo name (`payments-api`), or the named effort (`q3-vendor-onboarding`). Lowercase kebab-case, the **same string every time**, no ids, dates, versions or environments.- `step` = this specific decision: `deploy-v2.3-prod`, `drop-legacy-orders-table`. Example:request_decision(title: "Deploy v2.3 to prod?", question: "Ship v2.3 to production?", context: "…", workflow: "payments-api", step: "deploy-v2.3-prod")
A skill is still the model choosing to ask. For steps that must never run unapproved, add the gate hook as well.
#Enforce approval on specific tools (hook)
The tools above are model-elected: the agent chooses to call request_decision. To make approval non-bypassable for a set of tools, wire the gate command as a Claude Code PreToolUse hook. The hook's matcher is your list of gated tools: any matching call is intercepted, Ratifia opens a decision (as a Tool call panel showing exactly what would run), and the tool is blocked until a human approves, and denied otherwise.
In ~/.claude/settings.json (or a project .claude/settings.json):
{ "hooks": { "PreToolUse": [ { "matcher": "Bash|deploy_.*|delete_.*", "hooks": [ { "type": "command", "command": "npx -y @ratifia/mcp gate", "timeout": 300 } ] } ] }}
The hook command inherits your environment, so export the same config the MCP uses (RATIFIA_API_KEY, optionally RATIFIA_API_URL / RATIFIA_APPROVER_EMAIL / RATIFIA_POLICY).
| Variable | Purpose |
|---|---|
RATIFIA_GATED_TOOLS | Optional comma-separated allow-list checked inside the gate, letting you use a broad matcher and narrow here (e.g. deploy_prod,delete_database). Unset = gate everything the matcher sends. |
RATIFIA_GATE_TIMEOUT_SEC | How long to block waiting for a human (default 240). Keep the hook timeout a bit higher. |
RATIFIA_GATE_FAIL_OPEN | 1 = allow the tool if Ratifia is unreachable or the wait times out. Default is fail-closed (deny), because a gate that fails open is no gate. |
On approval the tool runs; on rejection it's blocked and the reviewer's note is returned to the agent as the reason. This is the enforcing complement to request_decision: the harness runs the hook whether or not the model chose to ask.
#FAQ
#My agent opened a decision and then never came back for the verdict.
This is the most common failure, and it is almost never a bug in Ratifia — the agent's turn simply ended while the decision was still open. A verdict does not wake a sleeping agent. request_decision hands back a decision_id and nothing pushes to the agent afterwards; if it doesn't hold the thread itself, the human answers into the dashboard and no one ever reads it.
Which way you hold it depends on how long the wait is:
| Your agent | Do this | Not this |
|---|---|---|
| Runs one long turn, human is at their keyboard | await_decision immediately after request_decision — it blocks until the verdict lands | Polling check_decision in a loop |
| Runs in discrete cycles (cron, queue worker, scheduled job) | Persist the decision_id to durable state, end the turn, and have the next cycle call check_decision | await_decision — it burns the whole cycle and the harness may kill it mid-wait |
The second row is the one that bites. If the human is away from their keyboard, a decision can sit for hours; an agent that blocks on it is an agent that is doing nothing else, and one that does not block but also doesn't record the decision_id has silently dropped the task on the floor.
If your harness has timers or scheduled wake-ups, set one before you call request_decision, not after — a turn can end before you get to the second call.
#How does an agent recover a decision it forgot about?
list_pending_decisions is the agent's inbox. Call it at the start of every run, before anything else. It is the only thing that closes the loop when an agent asked for a decision and then lost its context — compaction, a new session, a redeploy, a crashed process. Without it the agent either waits forever on a verdict it will never read, or re-asks and opens a duplicate decision for work that was already approved.
Treat a non-empty result as work to finish, not as noise.
#My agent went ahead and did the risky thing without asking.
request_decision is model-elected — the agent decides to call it, and an agent under pressure can decide not to. If a step must never run unapproved, don't rely on the model choosing well: wire the gate hook. The harness runs it whether or not the model thought to ask, and it fails closed.
#request_decision came back with approval_required: false. Did it break?
No — an org policy matched and auto-resolved that no human approval was needed. Proceed with the action. There is nothing to wait for and no decision to check.
#The human answered, but the agent ignored what they said.
The verdict is not the whole answer. Read the note — that is the human's actual reply, and it often changes how to do the approved thing. If you asked for a choice or a typed value, the answer is in response_value (map selected option ids back through response_spec.options), and if you passed a proposed_output, the text to act on is output.content — the human's refined version — not the draft you sent.
#Install options
Published on npm as @ratifia/mcp. The npx -y @ratifia/mcp config shown above fetches and runs it, no clone required.
For development, run from a local clone instead. Run npm install && npm run build, then point the entry at the built file:
{ "mcpServers": { "ratifia": { "command": "node", "args": ["/absolute/path/to/ratifia-mcp/dist/index.js"], "env": { "RATIFIA_API_KEY": "your-org-worker-api-key" } } }}