MCP · Reference

Dangerous-ops handshake

A short list of MCP tools never execute on the first call. The handler returns a confirm_token + a human-readable summary. The agent surfaces both to the user. The user confirms. The agent re-calls with the token. Only then does the action execute.

Why

Two reasons. First: agents acting on behalf of humans sometimes misinterpret intent. "Upgrade me to Pro" spoken to an agent shouldn't result in a charge without the user seeing the price. Second: even well-intentioned agents have bugs. The handshake makes money-moving + permanent state changes always require an explicit second touch.

Which tools are gated

Today, only the billing-mutation tools:

  • upgrade_plan
  • downgrade_plan

When we add new tools that move money or permanently widen access, they'll go through this same gate.

The two-call flow

Call 1, describe

{
  "tool": "upgrade_plan",
  "input": { "to": "scale" }
}

Response:

{
  "confirm_required": true,
  "confirm_token": "pbc_01J...",
  "summary": "Upgrade vector-build from Pro ($19/mo) to Scale ($49/mo). Pro-rated to today; first full Scale charge on May 30.",
  "ttl_seconds": 60
}

Surface to user

The agent shows the summary in chat verbatim. User reads it, replies "yes" / "confirm" / similar.

Call 2, execute

{
  "tool": "upgrade_plan",
  "input": {
    "to": "scale",
    "confirm_token": "pbc_01J..."
  }
}

Response:

{
  "ok": true,
  "subscription": { "plan": "scale", "currentPeriodEnd": "..." }
}

Confirm-token rules

  • Single use. Once consumed, the token is invalid.
  • 60-second TTL. Expires fast so a stale token can't reactivate later.
  • Bound to {org, principal, operation, params}. A token issued for upgrade-to-Scale by your agent for your org won't work for a different combination.
  • Idempotent on params change. If you re-call with different input, you get a new token + a new summary. The old token is invalidated.

Errors

  • consent_token_invalid: token doesn't exist or already used
  • consent_token_expired: older than 60 seconds
  • consent_token_mismatch: token was issued for a different operation or different params

Agent implementation pattern

async function upgradePlan(plan: "pro" | "scale") {
  // Call 1, get the confirm token + summary
  const r1 = await mcp.callTool("upgrade_plan", { to: plan });
  if (!r1.confirm_required) {
    // Already a no-op or trivially fast-pathed; done.
    return r1;
  }

  // Surface summary to the human, wait for explicit yes
  const userOk = await ask(`${r1.summary}\n\nProceed?`);
  if (!userOk) return { cancelled: true };

  // Call 2, execute
  return mcp.callTool("upgrade_plan", {
    to: plan,
    confirm_token: r1.confirm_token,
  });
}

REST equivalent

The same pattern applies to the REST endpoints behind these tools (POST /api/billing/upgrade, POST /api/billing/downgrade). Same handshake, same token shape.

Frequently asked questions

What is the dangerous-ops handshake in Dock?
A two-call confirmation pattern for MCP tools that move money or widen access. The first call returns a `confirm_token` + summary; the agent surfaces it to its user; the user confirms; the agent re-calls within 60s with the token. Single-use, scoped to the exact change.
Which Dock MCP tools require dangerous-ops confirmation?
Today: `upgrade_plan` and `downgrade_plan`. Future: any tool that moves money, deletes irreversibly at scale, or widens audience permanently. Tools are added by gating through `src/lib/billing-consent.ts`.
How do I confirm a Dock plan change my agent wants to make?
Two surfaces. `chat` mode: agent surfaces the `message` from the first call; you say yes; agent re-calls with `confirm_token` set. `web` mode: agent prints an `approval_url`; you click + approve in the browser; agent polls `polling_url` for the result.
How long is a Dock confirmation token valid?
60 seconds. Single-use. Bound to `{org, principal, operation, params}` so re-using the token for a different change is rejected. After 60s, the agent must repeat the first call to get a new token.
Can I bypass Dock's dangerous-ops confirmation for trusted agents?
No. The two-call confirmation is server-enforced and applies to every agent regardless of trust. The point is to stop runaway agents from auto-upgrading you to Scale or downgrading you to Free without your awareness; trust isn't a substitute for confirmation.
What's the difference between chat mode and web mode for dangerous ops?
`chat` (default): the agent shows the confirmation message in chat, you say yes, agent re-calls. Best for in-conversation flows. `web`: agent prints a click-to-approve URL, you approve in browser, agent polls. Best for headless agents or users who prefer click-to-approve.
How does my Dock agent handle the dangerous-ops handshake?
On the first call, expect `{ status: 'confirmation_required', confirm_token, message, expires_in }` (chat) or `{ status: 'approval_required', approval_url, polling_url, expires_at }` (web). Surface the appropriate UX to the user; on consent, re-call the tool with `confirm_token` (chat) or poll polling_url (web).
What if my agent calls a Dock dangerous-ops tool without the confirmation flow?
The first call ALWAYS returns the consent prompt; it never executes. The agent's responsibility is to surface the prompt to the user before re-calling. Agents that ignore the prompt and call again with no token just keep re-prompting; nothing happens until the user confirms.
Can I require dangerous-ops style confirmation for my own custom MCP tool?
Yes, gate it through `src/lib/billing-consent.ts::mintConsent` / `consumeConsent`. Wraps the same `PendingBillingConfirmation` model (single-use, 60s TTL, bound to params). Don't invent a parallel confirmation mechanism; the consent layer is the single audited gate.
What's the JSON-RPC error code for a Dock dangerous-ops cap or invalid token?
402 Payment Required (REST) maps to JSON-RPC error code -32015 (MCP). The error payload includes `details.upgrade` and `details.increase` next-step recommendations. Same shape across REST and MCP so agents handle both paths the same way.
Updated