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.