MCP server

Connect Shotoku to any MCP-compatible agent host — Claude, Cursor, Windsurf, or a custom agent — so that authorization happens automatically, mid-task, before your agent acts.

What is MCP?

MCP (Model Context Protocol) is an open standard that lets AI agents call external tools through a consistent interface. Instead of each agent needing custom integration code, MCP defines how tools are discovered, described, and called — so the same server works across Claude, Cursor, Windsurf, and any other MCP-compatible host.

When you add Shotoku as an MCP server, your agent gains direct access to its authorization tools. Before spending money, calling an API, or executing code, the agent can call authorize_action and let Shotoku make the call — automatically, inline, without you writing any integration glue.

The CLI is for you. The MCP server is for your agent.


Setup

1. Build the MCP server

From the repo root:

bash
pnpm build

This produces mcp/dist/index.js — the file your agent host will run.

2. Register Shotoku with your agent host

Most MCP hosts accept a JSON config file. Add a shotoku entry:

claude_desktop_config.json
{
  "mcpServers": {
    "shotoku": {
      "command": "node",
      "args": ["/absolute/path/to/shotoku/mcp/dist/index.js"],
      "env": {
        "SHOTOKU_POLICY": "/absolute/path/to/shotoku/policy.yaml",
        "SHOTOKU_LEDGER": "/absolute/path/to/shotoku/data/decisions.jsonl"
      }
    }
  }
}

Use absolute paths. Relative paths are resolved from the working directory where the server starts, which may differ by host.

For Claude Desktop, add this to ~/Library/Application Support/Claude/claude_desktop_config.json then restart the app.

3. Create the ledger directory

bash
mkdir -p data

Verifying the connection

In Claude Desktop, click the tools icon (hammer) in a new conversation and look for the shotoku section. You should see five tools:

  • authorize_action
  • get_decision
  • get_pending_approvals
  • approve_decision
  • deny_decision

The tools

authorize_action

The main gate. The agent calls this before performing any action — spending money, calling an API, running code, sending a message.

FieldRequiredWhat it means
actionyesWhat the agent wants to do (purchase, api_call, execute_code, send_email, mcp_tool, custom)
resourceyesWhat it's acting on — a domain, service name, or endpoint
amountnoHow much it costs in USD, if applicable
contextnoAny extra details you want recorded with the decision (must be JSON-serializable, max 16 KB)

There is deliberately no actor field. Identity comes from the connected client, not from the call — see Who the decision is attributed to below.

Example response
{
  "approved": true,
  "status": "approved",
  "reasons": [
    { "type": "policy_match", "text": "openai.com matched rule" },
    { "type": "limit_check",  "text": "Amount $5 within per-transaction limit of $50" },
    { "type": "budget_check", "text": "Daily budget remaining: $195" }
  ],
  "explanation": { "summary": "Request approved. All policy checks passed." },
  "decisionId": "dec_f3f0a6da3a69",
  "timestamp": "2026-06-22T15:02:00.000Z"
}

get_decision

Looks up a single past decision by ID. Returns the full record — original request, outcome, and reasons. Useful when the agent wants to reference what was decided earlier in a session.

get_pending_approvals

Returns every decision currently waiting for human review. The agent can surface this naturally: “You have 2 pending approvals — want to review them?” Returns an empty message when the queue is clear.

approve_decision

Approves a pending decision. Shotoku appends a new approval record to the ledger — it never mutates the original decision. Both the decision and the human approval are preserved in the audit trail.

json
{ "decisionId": "dec_abc123" }

deny_decision

Denies a pending decision. Same append-only behavior as approval.

json
{ "decisionId": "dec_abc123" }

Who the decision is attributed to

Every decision is recorded against an actor — the name of the agent that asked. Shotoku sets it; the agent does not.

An MCP client announces its own name during the initialize handshake, before it can make a single tool call, and the model on the other side never sees that value. A connection from Claude Desktop is recorded as claude-desktop, one from Cursor as cursor, with no configuration.

To name a server yourself — one entry per agent, each with its own budget and rules — set SHOTOKU_MCP_ACTORin that server’s config block:

json
{
  "mcpServers": {
    "shotoku-billing": {
      "command": "node",
      "args": ["/absolute/path/to/shotoku/mcp/dist/index.js"],
      "env": { "SHOTOKU_MCP_ACTOR": "billing-agent" }
    }
  }
}

Then write rules that apply to that agent alone:

policy.yaml
rules:
  - resource: "*"
    actor: billing-agent
    verdict: approved
    maxDailyAmount: 50

What this does and does not guarantee. It makes agents distinguishable, not unforgeable. An agent with shell access can edit a config file, or skip Shotoku altogether. The guarantee is narrower and still worth having: every call that goes through Shotoku is attributed to a named agent, and an agent cannot rename itself through the protocol. That is what makes the ledger worth reading.


Walkthrough

What a typical exchange looks like once Shotoku is connected:

bash
# You ask the agent something that involves an API call
You: Research the top three AI coding tools and summarize pricing.

# Agent calls authorize_action before doing anything
Agent → authorize_action({
  action: "api_call",
  resource: "openai.com", amount: 0.05
})

# Shotoku checks policy, records decision, returns approved
Shotoku → { approved: true, status: "approved", decisionId: "dec_..." }

# Agent proceeds
Agent: Here are the top three AI coding tools...

If your policy requires human approval:

bash
Agent: I need to call a service that isn't on your allowlist.
       Shotoku flagged this as pending approval.

# From your terminal:
shotoku approve dec_abc123
# → ✓ Approved. Recorded as apr_xxx.

Environment variables

VariableDefaultWhat it controls
SHOTOKU_CONFIGshotoku.config.jsonPath to the Shotoku config file
SHOTOKU_POLICYpolicy.yamlPath to your policy file (overrides config)
SHOTOKU_LEDGERdata/decisions.jsonlPath to the local decision ledger (overrides config)

If SHOTOKU_POLICY or SHOTOKU_LEDGER are set they take precedence. Otherwise the server reads shotoku.config.json. If no config file exists it falls back to the defaults above.