{
  "type": "stack",
  "id": "bookkeeping-agent",
  "title": "Bookkeeping agent stack",
  "summary": "An agent that reconciles Stripe or Paddle revenue against the books in Xero, reads receipts and statements, and prepares draft entries for an accountant. It runs on read-only credentials, and every write needs a human.",
  "author": "Agentica Author",
  "tags": [
    "bookkeeping",
    "accounting",
    "reconciliation",
    "finance",
    "mcp",
    "xero",
    "stripe",
    "paddle",
    "read-only",
    "human-in-the-loop"
  ],
  "published": "2026-11-02",
  "last_verified": "2026-11-02",
  "entries": [
    "claude-agent-sdk",
    "xero-mcp",
    "stripe-mcp",
    "paddle-mcp",
    "e2b",
    "markitdown",
    "langfuse",
    "plaid-mcp"
  ],
  "links": {
    "html": "https://indexagentica.com/stacks/bookkeeping-agent/",
    "markdown": "https://indexagentica.com/stacks/bookkeeping-agent.md",
    "json": "https://indexagentica.com/api/longform/stacks/bookkeeping-agent.json",
    "source": "https://github.com/Drudley/indexagentica/blob/main/content-long/stacks/bookkeeping-agent.md"
  },
  "status": "published",
  "stack": {
    "use_case": "Match payouts and invoices from Stripe or Paddle to what is recorded in Xero each month, explain every difference with the IDs behind it, and draft (never post) the fixes, with read-only credentials and a human approving any write.",
    "components": [
      {
        "role": "Agent harness",
        "entry": "claude-agent-sdk",
        "name": "Claude Agent SDK",
        "url": "https://indexagentica.com/entries/claude-agent-sdk/",
        "why": "Connects the MCP servers and decides which tool calls run. An allowlist plus dontAsk mode lets read tools run without prompts and denies everything else, and a deny rule can hide whole groups of write tools from the model."
      },
      {
        "role": "Accounting ledger",
        "entry": "xero-mcp",
        "name": "Xero MCP Server",
        "url": "https://indexagentica.com/entries/xero-mcp/",
        "why": "Xero's official local server covers invoices, payments, bank transactions, contacts, the chart of accounts and reports. It has no read-only switch, but it requests exactly the OAuth scopes in XERO_SCOPES, so read-only scopes keep it read-only at Xero's end."
      },
      {
        "role": "Card payments and invoices",
        "entry": "stripe-mcp",
        "name": "Stripe MCP Server",
        "url": "https://indexagentica.com/entries/stripe-mcp/",
        "why": "Stripe's hosted server reads payouts, balance transactions, charges, refunds, invoices and subscriptions through one stripe_api_read tool, with writes in a separate stripe_api_write tool you can deny. Agent-tagged restricted keys are subject to Stripe's approval rules."
      },
      {
        "role": "Subscription billing (merchant of record)",
        "entry": "paddle-mcp",
        "name": "Paddle MCP Server",
        "url": "https://indexagentica.com/entries/paddle-mcp/",
        "why": "For businesses that sell through Paddle Billing. --tools=read-only loads only read tools, and Paddle API key permissions return 403 for anything the key was not granted."
      },
      {
        "role": "Reconciliation sandbox",
        "entry": "e2b",
        "name": "E2B",
        "url": "https://indexagentica.com/entries/e2b/",
        "why": "Runs the matching and totals code on exported data, so the model does not do arithmetic in its head. Turn its internet access off once the data is loaded."
      },
      {
        "role": "Receipts and statements",
        "entry": "markitdown",
        "name": "MarkItDown",
        "url": "https://indexagentica.com/entries/markitdown/",
        "why": "Turns PDF and Excel statements, receipts and CSV exports into Markdown the agent can read, for documents that are not in any API."
      },
      {
        "role": "Tracing",
        "entry": "langfuse",
        "name": "Langfuse",
        "url": "https://indexagentica.com/entries/langfuse/",
        "why": "Keeps every tool call and result, so an accountant can see which Stripe and Xero records each proposed entry came from."
      }
    ]
  },
  "entries_detail": [
    {
      "id": "claude-agent-sdk",
      "name": "Claude Agent SDK",
      "summary": "Anthropic's SDK for building production agents with Claude Code as a library, in Python and TypeScript.",
      "url": "https://indexagentica.com/entries/claude-agent-sdk/",
      "json": "https://indexagentica.com/api/entries/claude-agent-sdk.json"
    },
    {
      "id": "xero-mcp",
      "name": "Xero MCP Server",
      "summary": "Xero's official local MCP server for its accounting API: contacts, invoices, chart of accounts, payroll and reports.",
      "url": "https://indexagentica.com/entries/xero-mcp/",
      "json": "https://indexagentica.com/api/entries/xero-mcp.json"
    },
    {
      "id": "stripe-mcp",
      "name": "Stripe MCP Server",
      "summary": "Stripe's hosted remote MCP server giving agents OAuth-secured access to the Stripe API and documentation.",
      "url": "https://indexagentica.com/entries/stripe-mcp/",
      "json": "https://indexagentica.com/api/entries/stripe-mcp.json"
    },
    {
      "id": "paddle-mcp",
      "name": "Paddle MCP Server",
      "summary": "Paddle's official MCP server for Paddle Billing: manage products, prices, customers, subscriptions, transactions, refunds and reports.",
      "url": "https://indexagentica.com/entries/paddle-mcp/",
      "json": "https://indexagentica.com/api/entries/paddle-mcp.json"
    },
    {
      "id": "e2b",
      "name": "E2B",
      "summary": "Open-source, secure cloud sandboxes for AI agents: an isolated machine per agent to run code, browse and use tools.",
      "url": "https://indexagentica.com/entries/e2b/",
      "json": "https://indexagentica.com/api/entries/e2b.json"
    },
    {
      "id": "markitdown",
      "name": "MarkItDown",
      "summary": "Microsoft's lightweight Python utility for converting PDF, Office, images, audio, HTML and more into structure-preserving Markdown for LLMs.",
      "url": "https://indexagentica.com/entries/markitdown/",
      "json": "https://indexagentica.com/api/entries/markitdown.json"
    },
    {
      "id": "langfuse",
      "name": "Langfuse",
      "summary": "Open-source agent evals and observability platform: trace, evaluate and improve LLM applications and agents.",
      "url": "https://indexagentica.com/entries/langfuse/",
      "json": "https://indexagentica.com/api/entries/langfuse.json"
    },
    {
      "id": "plaid-mcp",
      "name": "Plaid Dashboard MCP",
      "summary": "Plaid's hosted MCP server with Production diagnostics: debug Items, review Link conversion and check usage metrics.",
      "url": "https://indexagentica.com/entries/plaid-mcp/",
      "json": "https://indexagentica.com/api/entries/plaid-mcp.json"
    }
  ],
  "related": [
    {
      "type": "comparison",
      "id": "payment-processor-mcp-servers",
      "title": "Payment processor MCP servers compared",
      "url": "https://indexagentica.com/compare/payment-processor-mcp-servers/",
      "json": "https://indexagentica.com/api/longform/compare/payment-processor-mcp-servers.json"
    },
    {
      "type": "comparison",
      "id": "agent-payment-rails",
      "title": "Agent payment protocols compared",
      "url": "https://indexagentica.com/compare/agent-payment-rails/",
      "json": "https://indexagentica.com/api/longform/compare/agent-payment-rails.json"
    },
    {
      "type": "stack",
      "id": "agent-that-can-buy-things",
      "title": "Agent that can buy things",
      "url": "https://indexagentica.com/stacks/agent-that-can-buy-things/",
      "json": "https://indexagentica.com/api/longform/stacks/agent-that-can-buy-things.json"
    },
    {
      "type": "stack",
      "id": "data-analysis-agent",
      "title": "Data analysis agent stack",
      "url": "https://indexagentica.com/stacks/data-analysis-agent/",
      "json": "https://indexagentica.com/api/longform/stacks/data-analysis-agent.json"
    },
    {
      "type": "comparison",
      "id": "agent-identity-and-mcp-auth",
      "title": "Agent identity and MCP authorization platforms",
      "url": "https://indexagentica.com/compare/agent-identity-and-mcp-auth/",
      "json": "https://indexagentica.com/api/longform/compare/agent-identity-and-mcp-auth.json"
    },
    {
      "type": "guide",
      "id": "run-untrusted-code-in-a-sandbox",
      "title": "Run agent-generated code safely in a sandbox",
      "url": "https://indexagentica.com/guides/run-untrusted-code-in-a-sandbox/",
      "json": "https://indexagentica.com/api/longform/guides/run-untrusted-code-in-a-sandbox.json"
    },
    {
      "type": "guide",
      "id": "convert-documents-to-markdown-for-agents",
      "title": "Convert documents to Markdown for agents with MarkItDown and Docling",
      "url": "https://indexagentica.com/guides/convert-documents-to-markdown-for-agents/",
      "json": "https://indexagentica.com/api/longform/guides/convert-documents-to-markdown-for-agents.json"
    },
    {
      "type": "comparison",
      "id": "agent-observability-platforms",
      "title": "Observability and eval platforms for AI agents",
      "url": "https://indexagentica.com/compare/agent-observability-platforms/",
      "json": "https://indexagentica.com/api/longform/compare/agent-observability-platforms.json"
    },
    {
      "type": "guide",
      "id": "add-mcp-servers-to-claude-code",
      "title": "Add MCP servers to Claude Code",
      "url": "https://indexagentica.com/guides/add-mcp-servers-to-claude-code/",
      "json": "https://indexagentica.com/api/longform/guides/add-mcp-servers-to-claude-code.json"
    }
  ],
  "sources": [
    {
      "title": "Xero MCP Server README",
      "url": "https://github.com/XeroAPI/xero-mcp-server",
      "accessed": "2026-11-02"
    },
    {
      "title": "Xero MCP Server, client credentials and scopes (xero-client.ts)",
      "url": "https://github.com/XeroAPI/xero-mcp-server/blob/main/src/clients/xero-client.ts",
      "accessed": "2026-11-02"
    },
    {
      "title": "Xero Developer changelog",
      "url": "https://developer.xero.com/changelog",
      "accessed": "2026-11-02"
    },
    {
      "title": "Xero granular scopes FAQ",
      "url": "https://developer.xero.com/faq/granular-scopes",
      "accessed": "2026-11-02"
    },
    {
      "title": "Stripe MCP docs",
      "url": "https://docs.stripe.com/mcp",
      "accessed": "2026-11-02"
    },
    {
      "title": "Stripe API keys, agent-tagged keys",
      "url": "https://docs.stripe.com/keys#agent-keys",
      "accessed": "2026-11-02"
    },
    {
      "title": "Stripe two-party approvals",
      "url": "https://docs.stripe.com/account/approvals",
      "accessed": "2026-11-02"
    },
    {
      "title": "Stripe API, list balance transactions",
      "url": "https://docs.stripe.com/api/balance_transactions/list",
      "accessed": "2026-11-02"
    },
    {
      "title": "Stripe API, the payout object",
      "url": "https://docs.stripe.com/api/payouts/object",
      "accessed": "2026-11-02"
    },
    {
      "title": "Stripe supported currencies",
      "url": "https://docs.stripe.com/currencies",
      "accessed": "2026-11-02"
    },
    {
      "title": "Paddle MCP Server README",
      "url": "https://github.com/PaddleHQ/paddle-mcp-server",
      "accessed": "2026-11-02"
    },
    {
      "title": "Paddle API key permissions",
      "url": "https://developer.paddle.com/api-reference/about/permissions",
      "accessed": "2026-11-02"
    },
    {
      "title": "Claude Agent SDK, configure permissions",
      "url": "https://code.claude.com/docs/en/agent-sdk/permissions",
      "accessed": "2026-11-02"
    },
    {
      "title": "Claude Agent SDK, MCP",
      "url": "https://code.claude.com/docs/en/agent-sdk/mcp",
      "accessed": "2026-11-02"
    },
    {
      "title": "E2B docs, internet access",
      "url": "https://docs.e2b.dev/network/internet-access",
      "accessed": "2026-11-02"
    },
    {
      "title": "MarkItDown README",
      "url": "https://github.com/microsoft/markitdown",
      "accessed": "2026-11-02"
    },
    {
      "title": "Langfuse README",
      "url": "https://github.com/langfuse/langfuse",
      "accessed": "2026-11-02"
    },
    {
      "title": "Plaid Dashboard MCP docs",
      "url": "https://plaid.com/docs/resources/mcp/",
      "accessed": "2026-11-02"
    }
  ],
  "front_matter": {
    "id": "bookkeeping-agent",
    "type": "stack",
    "title": "Bookkeeping agent stack",
    "summary": "An agent that reconciles Stripe or Paddle revenue against the books in Xero, reads receipts and statements, and prepares draft entries for an accountant. It runs on read-only credentials, and every write needs a human.",
    "description": "A component list for a bookkeeping or month-end close agent built on the official Xero, Stripe and Paddle MCP servers, the Xero and Paddle ones added to the directory on 2026-11-01. It covers the read-only settings each server really supports (Xero OAuth scopes, Stripe agent-tagged restricted keys and approval rules, Paddle's read-only tool filter and API key permissions), a Claude Agent SDK allowlist that hides every write tool, which Xero writes create drafts and which post at once, and a tested matching snippet for Stripe payouts.",
    "author": "Agentica Author",
    "use_case": "Match payouts and invoices from Stripe or Paddle to what is recorded in Xero each month, explain every difference with the IDs behind it, and draft (never post) the fixes, with read-only credentials and a human approving any write.",
    "components": [
      {
        "role": "Agent harness",
        "entry": "claude-agent-sdk",
        "why": "Connects the MCP servers and decides which tool calls run. An allowlist plus dontAsk mode lets read tools run without prompts and denies everything else, and a deny rule can hide whole groups of write tools from the model."
      },
      {
        "role": "Accounting ledger",
        "entry": "xero-mcp",
        "why": "Xero's official local server covers invoices, payments, bank transactions, contacts, the chart of accounts and reports. It has no read-only switch, but it requests exactly the OAuth scopes in XERO_SCOPES, so read-only scopes keep it read-only at Xero's end."
      },
      {
        "role": "Card payments and invoices",
        "entry": "stripe-mcp",
        "why": "Stripe's hosted server reads payouts, balance transactions, charges, refunds, invoices and subscriptions through one stripe_api_read tool, with writes in a separate stripe_api_write tool you can deny. Agent-tagged restricted keys are subject to Stripe's approval rules."
      },
      {
        "role": "Subscription billing (merchant of record)",
        "entry": "paddle-mcp",
        "why": "For businesses that sell through Paddle Billing. --tools=read-only loads only read tools, and Paddle API key permissions return 403 for anything the key was not granted."
      },
      {
        "role": "Reconciliation sandbox",
        "entry": "e2b",
        "why": "Runs the matching and totals code on exported data, so the model does not do arithmetic in its head. Turn its internet access off once the data is loaded."
      },
      {
        "role": "Receipts and statements",
        "entry": "markitdown",
        "why": "Turns PDF and Excel statements, receipts and CSV exports into Markdown the agent can read, for documents that are not in any API."
      },
      {
        "role": "Tracing",
        "entry": "langfuse",
        "why": "Keeps every tool call and result, so an accountant can see which Stripe and Xero records each proposed entry came from."
      }
    ],
    "tags": [
      "bookkeeping",
      "accounting",
      "reconciliation",
      "finance",
      "mcp",
      "xero",
      "stripe",
      "paddle",
      "read-only",
      "human-in-the-loop"
    ],
    "entries": [
      "claude-agent-sdk",
      "xero-mcp",
      "stripe-mcp",
      "paddle-mcp",
      "e2b",
      "markitdown",
      "langfuse",
      "plaid-mcp"
    ],
    "related": [
      "payment-processor-mcp-servers",
      "agent-payment-rails",
      "agent-that-can-buy-things",
      "data-analysis-agent",
      "agent-identity-and-mcp-auth",
      "run-untrusted-code-in-a-sandbox",
      "convert-documents-to-markdown-for-agents",
      "agent-observability-platforms",
      "add-mcp-servers-to-claude-code"
    ],
    "sources": [
      {
        "title": "Xero MCP Server README",
        "url": "https://github.com/XeroAPI/xero-mcp-server",
        "accessed": "2026-11-02"
      },
      {
        "title": "Xero MCP Server, client credentials and scopes (xero-client.ts)",
        "url": "https://github.com/XeroAPI/xero-mcp-server/blob/main/src/clients/xero-client.ts",
        "accessed": "2026-11-02"
      },
      {
        "title": "Xero Developer changelog",
        "url": "https://developer.xero.com/changelog",
        "accessed": "2026-11-02"
      },
      {
        "title": "Xero granular scopes FAQ",
        "url": "https://developer.xero.com/faq/granular-scopes",
        "accessed": "2026-11-02"
      },
      {
        "title": "Stripe MCP docs",
        "url": "https://docs.stripe.com/mcp",
        "accessed": "2026-11-02"
      },
      {
        "title": "Stripe API keys, agent-tagged keys",
        "url": "https://docs.stripe.com/keys#agent-keys",
        "accessed": "2026-11-02"
      },
      {
        "title": "Stripe two-party approvals",
        "url": "https://docs.stripe.com/account/approvals",
        "accessed": "2026-11-02"
      },
      {
        "title": "Stripe API, list balance transactions",
        "url": "https://docs.stripe.com/api/balance_transactions/list",
        "accessed": "2026-11-02"
      },
      {
        "title": "Stripe API, the payout object",
        "url": "https://docs.stripe.com/api/payouts/object",
        "accessed": "2026-11-02"
      },
      {
        "title": "Stripe supported currencies",
        "url": "https://docs.stripe.com/currencies",
        "accessed": "2026-11-02"
      },
      {
        "title": "Paddle MCP Server README",
        "url": "https://github.com/PaddleHQ/paddle-mcp-server",
        "accessed": "2026-11-02"
      },
      {
        "title": "Paddle API key permissions",
        "url": "https://developer.paddle.com/api-reference/about/permissions",
        "accessed": "2026-11-02"
      },
      {
        "title": "Claude Agent SDK, configure permissions",
        "url": "https://code.claude.com/docs/en/agent-sdk/permissions",
        "accessed": "2026-11-02"
      },
      {
        "title": "Claude Agent SDK, MCP",
        "url": "https://code.claude.com/docs/en/agent-sdk/mcp",
        "accessed": "2026-11-02"
      },
      {
        "title": "E2B docs, internet access",
        "url": "https://docs.e2b.dev/network/internet-access",
        "accessed": "2026-11-02"
      },
      {
        "title": "MarkItDown README",
        "url": "https://github.com/microsoft/markitdown",
        "accessed": "2026-11-02"
      },
      {
        "title": "Langfuse README",
        "url": "https://github.com/langfuse/langfuse",
        "accessed": "2026-11-02"
      },
      {
        "title": "Plaid Dashboard MCP docs",
        "url": "https://plaid.com/docs/resources/mcp/",
        "accessed": "2026-11-02"
      }
    ],
    "last_verified": "2026-11-02",
    "published": "2026-11-02"
  },
  "markdown": "\n## How the pieces fit\n\nMonth-end bookkeeping is mostly matching. Money arrives through Stripe or Paddle, it lands in the bank, and it should show up in Xero as paid invoices and bank transactions. The agent's job is to find what doesn't match and explain why: a fee that wasn't booked, a refund with no credit note, a payout split across two bank lines. An accountant then decides what to post.\n\n1. **Pull the month's money movements.** From [Stripe](https://indexagentica.com/entries/stripe-mcp/), list payouts, then the balance transactions in each one. The list endpoint takes a `payout` filter, which works for automatic payouts only. The payout object says so in `reconciliation_status`, which is `completed` when that list is available. Each balance transaction has `amount`, `fee` and `net`, where `net` is `amount` minus `fee`. From [Paddle](https://indexagentica.com/entries/paddle-mcp/), list transactions and adjustments (refunds and credits), or read an existing CSV report.\n2. **Pull what the books say.** From [Xero](https://indexagentica.com/entries/xero-mcp/), list invoices, payments, bank transactions (each is marked reconciled or unreconciled), the chart of accounts and tax rates, and a trial balance or P&L for the period.\n3. **Match outside the model.** Export both sides to files and run the matching in the [E2B](https://indexagentica.com/entries/e2b/) sandbox, not in the conversation. The snippet below is a starting point.\n4. **Read the documents that have no API.** Bank statement PDFs, supplier receipts and spreadsheets go through [MarkItDown](https://indexagentica.com/entries/markitdown/).\n5. **Report, then draft.** The output is a list of matched items, unmatched items with the IDs on both sides, and proposed entries. Writing those entries is a separate, approved step (see below). [Langfuse](https://indexagentica.com/entries/langfuse/) keeps the trace, so each proposal can be checked against the records it came from.\n\n## Keep it read-only\n\nEach of the three servers handles \"read-only\" differently, so set it up at both ends: in the credential, and in the harness.\n\n- **Xero: scopes.** The server has no read-only flag; on a test run it listed 51 tools, 25 of which create, update, add, approve, revert or delete. Without `XERO_SCOPES`, it asks Xero for its default scope list, which includes read-write scopes such as `accounting.invoices` and `accounting.payments`. When `XERO_SCOPES` is set, it requests exactly those scopes and nothing else. The server's README lists the `.read` versions of the invoice, payment, bank transaction and report scopes. Create the Custom Connection with read scopes only and set `XERO_SCOPES` to the same list. Xero's FAQ says a call that needs a scope you haven't been granted gets a 401 with an insufficient-scope `WWW-Authenticate` header. Test with a Demo Company first.\n- **Stripe: agent key and approval rules.** Stripe recommends OAuth for interactive clients. For an unattended agent, use an agent-tagged restricted key with read permissions on the resources you need. From December 2026, the MCP server stops accepting full-access secret keys and restricted keys without the Agent tag. Agent-tagged keys are subject to approval rules, and Stripe adds default rules for refunds, subscription cancellations and outbound payments. You can add rules (approve or block) for payouts, credit notes, customer balance transactions and more.\n- **Paddle: tool filter and key permissions.** The default `--tools` setting is `non-destructive`, and that is not read-only. On a test run, `non-destructive` loaded 63 tools, including `create_transaction`, `create_adjustment` (refunds and credits) and `create_subscription_charge`. `read-only` loaded 41 tools. Use `--tools=read-only` and give the API key only `.read` permissions. Note that `create_report` is not a read-only tool. If you want the agent to start new CSV exports, add `create_report` to an explicit comma-separated tool list (42 tools), or let a person create the report and have the agent read it.\n\nIn the harness, auto-approve only read tools and hide the write tools. MCP tools are named `mcp__<server>__<tool>`. Allow rules accept a glob after the server name, and deny rules both block calls and remove the tool definitions. In `dontAsk` mode, any call that isn't approved is denied instead of prompting.\n\n```python\nimport os\nfrom claude_agent_sdk import ClaudeAgentOptions\n\n# Read-only Xero scopes. Give the Custom Connection only these, too.\nXERO_READ_SCOPES = \" \".join([\n    \"accounting.invoices.read\", \"accounting.payments.read\",\n    \"accounting.banktransactions.read\", \"accounting.contacts.read\",\n    \"accounting.settings.read\", \"accounting.reports.profitandloss.read\",\n    \"accounting.reports.trialbalance.read\",\n])\n\noptions = ClaudeAgentOptions(\n    mcp_servers={\n        \"xero\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@xeroapi/xero-mcp-server@0.0.17\"],\n            \"env\": {\n                \"XERO_CLIENT_ID\": os.environ[\"XERO_CLIENT_ID\"],\n                \"XERO_CLIENT_SECRET\": os.environ[\"XERO_CLIENT_SECRET\"],\n                \"XERO_SCOPES\": XERO_READ_SCOPES,\n            },\n        },\n        \"stripe\": {\n            \"type\": \"http\",\n            \"url\": \"https://mcp.stripe.com\",\n            # An agent-tagged restricted key with read permissions only\n            \"headers\": {\"Authorization\": f\"Bearer {os.environ['STRIPE_AGENT_KEY']}\"},\n        },\n    },\n    allowed_tools=[\n        \"mcp__xero__list-*\", \"mcp__xero__get-*\",\n        \"mcp__stripe__stripe_api_search\", \"mcp__stripe__stripe_api_details\",\n        \"mcp__stripe__stripe_api_read\",\n    ],\n    disallowed_tools=[\n        \"mcp__xero__create-*\", \"mcp__xero__update-*\", \"mcp__xero__delete-*\",\n        \"mcp__xero__approve-*\", \"mcp__xero__revert-*\", \"mcp__xero__add-*\",\n        \"mcp__stripe__stripe_api_write\", \"mcp__stripe__send_money\",\n        \"mcp__stripe__transfer_money\", \"mcp__stripe__convert_currency\",\n    ],\n    permission_mode=\"dontAsk\",\n)\n```\n\nFor Paddle, add a server running `npx -y @paddle/paddle-mcp@0.1.6 --tools=read-only` with `PADDLE_API_KEY` and `PADDLE_ENVIRONMENT` in its `env`. Every tool it loads is then read-only, so `mcp__paddle__*` can go in the allowlist.\n\n## Writes: drafts first, and a human for each one\n\nWhen the report is accepted, the fixes can be written, but not by the same unattended run. Use a second configuration with write scopes, and leave write tools out of `allowed_tools`, so each call reaches your `canUseTool` callback for a person to approve. That only works in `default` mode; `dontAsk` denies the call instead. Know what each Xero write does. In version 0.0.17 of the server:\n\n- `create-invoice`, `create-credit-note` and `create-quote` always create **drafts**, and `create-manual-journal` defaults to draft. An accountant can review and approve these in Xero.\n- `create-bank-transaction` creates the transaction as **authorised**, and `create-payment` records a payment against an authorised invoice straight away. Treat these as posting entries.\n- The server has no tool for bank statement lines or for reconciling them. Matching a statement line in Xero stays a job for a person in Xero itself.\n\nOn the Stripe side, writes go through `stripe_api_write`. Stripe can require human confirmation in the Dashboard for some actions, such as refunds, and your own approval rules can add others or block them outright.\n\n## Match payouts outside the model\n\nThe snippet below matches Stripe payouts to Xero bank transactions that receive money. A match needs the exact amount and an arrival date within a few days. Anything with zero or several candidates is sent to a person instead of being guessed. Stripe amounts are integers in the currency's minor unit, so it divides by 100. That is only right for two-decimal currencies; [zero-decimal currencies](https://docs.stripe.com/currencies#zero-decimal) such as JPY need no division.\n\n```python\nfrom decimal import Decimal\n\ndef match_payouts(payouts, bank_lines, days=3):\n    \"\"\"payouts: Stripe payout dicts (amount in minor units, arrival_date as a date).\n    bank_lines: Xero RECEIVE bank transactions as dicts with total (Decimal), date and id.\n    Returns (matches, unmatched_payouts, unmatched_lines). Exact amount, date within +-days.\"\"\"\n    free = list(bank_lines)\n    matches, missing = [], []\n    for p in sorted(payouts, key=lambda p: p[\"arrival_date\"]):\n        want = Decimal(p[\"amount\"]) / 100  # two-decimal currencies only\n        hits = [b for b in free if b[\"total\"] == want\n                and abs((b[\"date\"] - p[\"arrival_date\"]).days) <= days]\n        if len(hits) == 1:\n            matches.append((p[\"id\"], hits[0][\"id\"]))\n            free.remove(hits[0])\n        else:\n            missing.append((p[\"id\"], f\"{len(hits)} candidate lines\"))\n    return matches, missing, [b[\"id\"] for b in free]\n```\n\nFor each payout that matches, check the next level down: the payout's balance transactions against the Xero invoices and payments for the same customers. That is where unbooked fees and refunds show up.\n\nXero's `list-bank-transactions` tool returns 10 transactions per page as text, newest first. A busy month takes many calls, and every result passes through the model's context. Have the agent page through once, write what it reads to a file, and do the rest of the work in the sandbox.\n\n## Test before you connect live books\n\n- **Xero:** start with the Demo Company, as Xero's README recommends. It comes with sample data and can be reset.\n- **Stripe:** use a sandbox. OAuth consent lets you grant sandboxes and live accounts separate permissions.\n- **Paddle:** set `PADDLE_ENVIRONMENT=sandbox` with a sandbox key.\n- Keep a month you have already closed by hand as a fixed test case. Rerun the agent on it whenever you change the prompt, the model or a server version, and compare its list of differences with the one your accountant produced.\n\n## Variations\n\n- **Xero's hosted server.** On 2026-10-15 Xero launched a Developer Remote MCP with 39 tools across invoicing, accounting, reporting and payments, for commercial apps on the Core tier or above. The local server above needs no app tier, only a Custom Connection or a bearer token.\n- **Bank data.** The [Plaid Dashboard MCP](https://indexagentica.com/entries/plaid-mcp/) is for diagnosing a Plaid integration (Items, Link conversion, usage), not for reading account transactions, so it is not a bank feed for this stack.\n- **Other processors.** Square, PayPal, Razorpay and Adyen have their own MCP servers; the [payment processor MCP servers comparison](https://indexagentica.com/compare/payment-processor-mcp-servers/) covers what each can read and write.\n- **Agents that spend money** are a different stack: see [Agent that can buy things](https://indexagentica.com/stacks/agent-that-can-buy-things/).\n",
  "raw": "---\nid: bookkeeping-agent\ntype: stack\ntitle: Bookkeeping agent stack\nsummary: \"An agent that reconciles Stripe or Paddle revenue against the books in Xero, reads receipts and statements, and prepares draft entries for an accountant. It runs on read-only credentials, and every write needs a human.\"\ndescription: \"A component list for a bookkeeping or month-end close agent built on the official Xero, Stripe and Paddle MCP servers, the Xero and Paddle ones added to the directory on 2026-11-01. It covers the read-only settings each server really supports (Xero OAuth scopes, Stripe agent-tagged restricted keys and approval rules, Paddle's read-only tool filter and API key permissions), a Claude Agent SDK allowlist that hides every write tool, which Xero writes create drafts and which post at once, and a tested matching snippet for Stripe payouts.\"\nauthor: Agentica Author\nuse_case: \"Match payouts and invoices from Stripe or Paddle to what is recorded in Xero each month, explain every difference with the IDs behind it, and draft (never post) the fixes, with read-only credentials and a human approving any write.\"\ncomponents:\n  - role: Agent harness\n    entry: claude-agent-sdk\n    why: \"Connects the MCP servers and decides which tool calls run. An allowlist plus dontAsk mode lets read tools run without prompts and denies everything else, and a deny rule can hide whole groups of write tools from the model.\"\n  - role: Accounting ledger\n    entry: xero-mcp\n    why: \"Xero's official local server covers invoices, payments, bank transactions, contacts, the chart of accounts and reports. It has no read-only switch, but it requests exactly the OAuth scopes in XERO_SCOPES, so read-only scopes keep it read-only at Xero's end.\"\n  - role: Card payments and invoices\n    entry: stripe-mcp\n    why: \"Stripe's hosted server reads payouts, balance transactions, charges, refunds, invoices and subscriptions through one stripe_api_read tool, with writes in a separate stripe_api_write tool you can deny. Agent-tagged restricted keys are subject to Stripe's approval rules.\"\n  - role: Subscription billing (merchant of record)\n    entry: paddle-mcp\n    why: \"For businesses that sell through Paddle Billing. --tools=read-only loads only read tools, and Paddle API key permissions return 403 for anything the key was not granted.\"\n  - role: Reconciliation sandbox\n    entry: e2b\n    why: \"Runs the matching and totals code on exported data, so the model does not do arithmetic in its head. Turn its internet access off once the data is loaded.\"\n  - role: Receipts and statements\n    entry: markitdown\n    why: \"Turns PDF and Excel statements, receipts and CSV exports into Markdown the agent can read, for documents that are not in any API.\"\n  - role: Tracing\n    entry: langfuse\n    why: \"Keeps every tool call and result, so an accountant can see which Stripe and Xero records each proposed entry came from.\"\ntags: [bookkeeping, accounting, reconciliation, finance, mcp, xero, stripe, paddle, read-only, human-in-the-loop]\nentries: [claude-agent-sdk, xero-mcp, stripe-mcp, paddle-mcp, e2b, markitdown, langfuse, plaid-mcp]\nrelated: [payment-processor-mcp-servers, agent-payment-rails, agent-that-can-buy-things, data-analysis-agent, agent-identity-and-mcp-auth, run-untrusted-code-in-a-sandbox, convert-documents-to-markdown-for-agents, agent-observability-platforms, add-mcp-servers-to-claude-code]\nsources:\n  - {title: Xero MCP Server README, url: \"https://github.com/XeroAPI/xero-mcp-server\", accessed: 2026-11-02}\n  - {title: \"Xero MCP Server, client credentials and scopes (xero-client.ts)\", url: \"https://github.com/XeroAPI/xero-mcp-server/blob/main/src/clients/xero-client.ts\", accessed: 2026-11-02}\n  - {title: Xero Developer changelog, url: \"https://developer.xero.com/changelog\", accessed: 2026-11-02}\n  - {title: Xero granular scopes FAQ, url: \"https://developer.xero.com/faq/granular-scopes\", accessed: 2026-11-02}\n  - {title: Stripe MCP docs, url: \"https://docs.stripe.com/mcp\", accessed: 2026-11-02}\n  - {title: \"Stripe API keys, agent-tagged keys\", url: \"https://docs.stripe.com/keys#agent-keys\", accessed: 2026-11-02}\n  - {title: Stripe two-party approvals, url: \"https://docs.stripe.com/account/approvals\", accessed: 2026-11-02}\n  - {title: \"Stripe API, list balance transactions\", url: \"https://docs.stripe.com/api/balance_transactions/list\", accessed: 2026-11-02}\n  - {title: \"Stripe API, the payout object\", url: \"https://docs.stripe.com/api/payouts/object\", accessed: 2026-11-02}\n  - {title: Stripe supported currencies, url: \"https://docs.stripe.com/currencies\", accessed: 2026-11-02}\n  - {title: Paddle MCP Server README, url: \"https://github.com/PaddleHQ/paddle-mcp-server\", accessed: 2026-11-02}\n  - {title: Paddle API key permissions, url: \"https://developer.paddle.com/api-reference/about/permissions\", accessed: 2026-11-02}\n  - {title: \"Claude Agent SDK, configure permissions\", url: \"https://code.claude.com/docs/en/agent-sdk/permissions\", accessed: 2026-11-02}\n  - {title: \"Claude Agent SDK, MCP\", url: \"https://code.claude.com/docs/en/agent-sdk/mcp\", accessed: 2026-11-02}\n  - {title: \"E2B docs, internet access\", url: \"https://docs.e2b.dev/network/internet-access\", accessed: 2026-11-02}\n  - {title: MarkItDown README, url: \"https://github.com/microsoft/markitdown\", accessed: 2026-11-02}\n  - {title: Langfuse README, url: \"https://github.com/langfuse/langfuse\", accessed: 2026-11-02}\n  - {title: Plaid Dashboard MCP docs, url: \"https://plaid.com/docs/resources/mcp/\", accessed: 2026-11-02}\nlast_verified: 2026-11-02\npublished: 2026-11-02\n---\n\n## How the pieces fit\n\nMonth-end bookkeeping is mostly matching. Money arrives through Stripe or Paddle, it lands in the bank, and it should show up in Xero as paid invoices and bank transactions. The agent's job is to find what doesn't match and explain why: a fee that wasn't booked, a refund with no credit note, a payout split across two bank lines. An accountant then decides what to post.\n\n1. **Pull the month's money movements.** From [Stripe](https://indexagentica.com/entries/stripe-mcp/), list payouts, then the balance transactions in each one. The list endpoint takes a `payout` filter, which works for automatic payouts only. The payout object says so in `reconciliation_status`, which is `completed` when that list is available. Each balance transaction has `amount`, `fee` and `net`, where `net` is `amount` minus `fee`. From [Paddle](https://indexagentica.com/entries/paddle-mcp/), list transactions and adjustments (refunds and credits), or read an existing CSV report.\n2. **Pull what the books say.** From [Xero](https://indexagentica.com/entries/xero-mcp/), list invoices, payments, bank transactions (each is marked reconciled or unreconciled), the chart of accounts and tax rates, and a trial balance or P&L for the period.\n3. **Match outside the model.** Export both sides to files and run the matching in the [E2B](https://indexagentica.com/entries/e2b/) sandbox, not in the conversation. The snippet below is a starting point.\n4. **Read the documents that have no API.** Bank statement PDFs, supplier receipts and spreadsheets go through [MarkItDown](https://indexagentica.com/entries/markitdown/).\n5. **Report, then draft.** The output is a list of matched items, unmatched items with the IDs on both sides, and proposed entries. Writing those entries is a separate, approved step (see below). [Langfuse](https://indexagentica.com/entries/langfuse/) keeps the trace, so each proposal can be checked against the records it came from.\n\n## Keep it read-only\n\nEach of the three servers handles \"read-only\" differently, so set it up at both ends: in the credential, and in the harness.\n\n- **Xero: scopes.** The server has no read-only flag; on a test run it listed 51 tools, 25 of which create, update, add, approve, revert or delete. Without `XERO_SCOPES`, it asks Xero for its default scope list, which includes read-write scopes such as `accounting.invoices` and `accounting.payments`. When `XERO_SCOPES` is set, it requests exactly those scopes and nothing else. The server's README lists the `.read` versions of the invoice, payment, bank transaction and report scopes. Create the Custom Connection with read scopes only and set `XERO_SCOPES` to the same list. Xero's FAQ says a call that needs a scope you haven't been granted gets a 401 with an insufficient-scope `WWW-Authenticate` header. Test with a Demo Company first.\n- **Stripe: agent key and approval rules.** Stripe recommends OAuth for interactive clients. For an unattended agent, use an agent-tagged restricted key with read permissions on the resources you need. From December 2026, the MCP server stops accepting full-access secret keys and restricted keys without the Agent tag. Agent-tagged keys are subject to approval rules, and Stripe adds default rules for refunds, subscription cancellations and outbound payments. You can add rules (approve or block) for payouts, credit notes, customer balance transactions and more.\n- **Paddle: tool filter and key permissions.** The default `--tools` setting is `non-destructive`, and that is not read-only. On a test run, `non-destructive` loaded 63 tools, including `create_transaction`, `create_adjustment` (refunds and credits) and `create_subscription_charge`. `read-only` loaded 41 tools. Use `--tools=read-only` and give the API key only `.read` permissions. Note that `create_report` is not a read-only tool. If you want the agent to start new CSV exports, add `create_report` to an explicit comma-separated tool list (42 tools), or let a person create the report and have the agent read it.\n\nIn the harness, auto-approve only read tools and hide the write tools. MCP tools are named `mcp__<server>__<tool>`. Allow rules accept a glob after the server name, and deny rules both block calls and remove the tool definitions. In `dontAsk` mode, any call that isn't approved is denied instead of prompting.\n\n```python\nimport os\nfrom claude_agent_sdk import ClaudeAgentOptions\n\n# Read-only Xero scopes. Give the Custom Connection only these, too.\nXERO_READ_SCOPES = \" \".join([\n    \"accounting.invoices.read\", \"accounting.payments.read\",\n    \"accounting.banktransactions.read\", \"accounting.contacts.read\",\n    \"accounting.settings.read\", \"accounting.reports.profitandloss.read\",\n    \"accounting.reports.trialbalance.read\",\n])\n\noptions = ClaudeAgentOptions(\n    mcp_servers={\n        \"xero\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@xeroapi/xero-mcp-server@0.0.17\"],\n            \"env\": {\n                \"XERO_CLIENT_ID\": os.environ[\"XERO_CLIENT_ID\"],\n                \"XERO_CLIENT_SECRET\": os.environ[\"XERO_CLIENT_SECRET\"],\n                \"XERO_SCOPES\": XERO_READ_SCOPES,\n            },\n        },\n        \"stripe\": {\n            \"type\": \"http\",\n            \"url\": \"https://mcp.stripe.com\",\n            # An agent-tagged restricted key with read permissions only\n            \"headers\": {\"Authorization\": f\"Bearer {os.environ['STRIPE_AGENT_KEY']}\"},\n        },\n    },\n    allowed_tools=[\n        \"mcp__xero__list-*\", \"mcp__xero__get-*\",\n        \"mcp__stripe__stripe_api_search\", \"mcp__stripe__stripe_api_details\",\n        \"mcp__stripe__stripe_api_read\",\n    ],\n    disallowed_tools=[\n        \"mcp__xero__create-*\", \"mcp__xero__update-*\", \"mcp__xero__delete-*\",\n        \"mcp__xero__approve-*\", \"mcp__xero__revert-*\", \"mcp__xero__add-*\",\n        \"mcp__stripe__stripe_api_write\", \"mcp__stripe__send_money\",\n        \"mcp__stripe__transfer_money\", \"mcp__stripe__convert_currency\",\n    ],\n    permission_mode=\"dontAsk\",\n)\n```\n\nFor Paddle, add a server running `npx -y @paddle/paddle-mcp@0.1.6 --tools=read-only` with `PADDLE_API_KEY` and `PADDLE_ENVIRONMENT` in its `env`. Every tool it loads is then read-only, so `mcp__paddle__*` can go in the allowlist.\n\n## Writes: drafts first, and a human for each one\n\nWhen the report is accepted, the fixes can be written, but not by the same unattended run. Use a second configuration with write scopes, and leave write tools out of `allowed_tools`, so each call reaches your `canUseTool` callback for a person to approve. That only works in `default` mode; `dontAsk` denies the call instead. Know what each Xero write does. In version 0.0.17 of the server:\n\n- `create-invoice`, `create-credit-note` and `create-quote` always create **drafts**, and `create-manual-journal` defaults to draft. An accountant can review and approve these in Xero.\n- `create-bank-transaction` creates the transaction as **authorised**, and `create-payment` records a payment against an authorised invoice straight away. Treat these as posting entries.\n- The server has no tool for bank statement lines or for reconciling them. Matching a statement line in Xero stays a job for a person in Xero itself.\n\nOn the Stripe side, writes go through `stripe_api_write`. Stripe can require human confirmation in the Dashboard for some actions, such as refunds, and your own approval rules can add others or block them outright.\n\n## Match payouts outside the model\n\nThe snippet below matches Stripe payouts to Xero bank transactions that receive money. A match needs the exact amount and an arrival date within a few days. Anything with zero or several candidates is sent to a person instead of being guessed. Stripe amounts are integers in the currency's minor unit, so it divides by 100. That is only right for two-decimal currencies; [zero-decimal currencies](https://docs.stripe.com/currencies#zero-decimal) such as JPY need no division.\n\n```python\nfrom decimal import Decimal\n\ndef match_payouts(payouts, bank_lines, days=3):\n    \"\"\"payouts: Stripe payout dicts (amount in minor units, arrival_date as a date).\n    bank_lines: Xero RECEIVE bank transactions as dicts with total (Decimal), date and id.\n    Returns (matches, unmatched_payouts, unmatched_lines). Exact amount, date within +-days.\"\"\"\n    free = list(bank_lines)\n    matches, missing = [], []\n    for p in sorted(payouts, key=lambda p: p[\"arrival_date\"]):\n        want = Decimal(p[\"amount\"]) / 100  # two-decimal currencies only\n        hits = [b for b in free if b[\"total\"] == want\n                and abs((b[\"date\"] - p[\"arrival_date\"]).days) <= days]\n        if len(hits) == 1:\n            matches.append((p[\"id\"], hits[0][\"id\"]))\n            free.remove(hits[0])\n        else:\n            missing.append((p[\"id\"], f\"{len(hits)} candidate lines\"))\n    return matches, missing, [b[\"id\"] for b in free]\n```\n\nFor each payout that matches, check the next level down: the payout's balance transactions against the Xero invoices and payments for the same customers. That is where unbooked fees and refunds show up.\n\nXero's `list-bank-transactions` tool returns 10 transactions per page as text, newest first. A busy month takes many calls, and every result passes through the model's context. Have the agent page through once, write what it reads to a file, and do the rest of the work in the sandbox.\n\n## Test before you connect live books\n\n- **Xero:** start with the Demo Company, as Xero's README recommends. It comes with sample data and can be reset.\n- **Stripe:** use a sandbox. OAuth consent lets you grant sandboxes and live accounts separate permissions.\n- **Paddle:** set `PADDLE_ENVIRONMENT=sandbox` with a sandbox key.\n- Keep a month you have already closed by hand as a fixed test case. Rerun the agent on it whenever you change the prompt, the model or a server version, and compare its list of differences with the one your accountant produced.\n\n## Variations\n\n- **Xero's hosted server.** On 2026-10-15 Xero launched a Developer Remote MCP with 39 tools across invoicing, accounting, reporting and payments, for commercial apps on the Core tier or above. The local server above needs no app tier, only a Custom Connection or a bearer token.\n- **Bank data.** The [Plaid Dashboard MCP](https://indexagentica.com/entries/plaid-mcp/) is for diagnosing a Plaid integration (Items, Link conversion, usage), not for reading account transactions, so it is not a bank feed for this stack.\n- **Other processors.** Square, PayPal, Razorpay and Adyen have their own MCP servers; the [payment processor MCP servers comparison](https://indexagentica.com/compare/payment-processor-mcp-servers/) covers what each can read and write.\n- **Agents that spend money** are a different stack: see [Agent that can buy things](https://indexagentica.com/stacks/agent-that-can-buy-things/).\n",
  "html": "<h2>How the pieces fit</h2>\n<p>Month-end bookkeeping is mostly matching. Money arrives through Stripe or Paddle, it lands in the bank, and it should show up in Xero as paid invoices and bank transactions. The agent&#39;s job is to find what doesn&#39;t match and explain why: a fee that wasn&#39;t booked, a refund with no credit note, a payout split across two bank lines. An accountant then decides what to post.</p>\n<ol><li><strong>Pull the month&#39;s money movements.</strong> From <a href=\"/entries/stripe-mcp/\">Stripe</a>, list payouts, then the balance transactions in each one. The list endpoint takes a <code>payout</code> filter, which works for automatic payouts only. The payout object says so in <code>reconciliation_status</code>, which is <code>completed</code> when that list is available. Each balance transaction has <code>amount</code>, <code>fee</code> and <code>net</code>, where <code>net</code> is <code>amount</code> minus <code>fee</code>. From <a href=\"/entries/paddle-mcp/\">Paddle</a>, list transactions and adjustments (refunds and credits), or read an existing CSV report.</li><li><strong>Pull what the books say.</strong> From <a href=\"/entries/xero-mcp/\">Xero</a>, list invoices, payments, bank transactions (each is marked reconciled or unreconciled), the chart of accounts and tax rates, and a trial balance or P&amp;L for the period.</li><li><strong>Match outside the model.</strong> Export both sides to files and run the matching in the <a href=\"/entries/e2b/\">E2B</a> sandbox, not in the conversation. The snippet below is a starting point.</li><li><strong>Read the documents that have no API.</strong> Bank statement PDFs, supplier receipts and spreadsheets go through <a href=\"/entries/markitdown/\">MarkItDown</a>.</li><li><strong>Report, then draft.</strong> The output is a list of matched items, unmatched items with the IDs on both sides, and proposed entries. Writing those entries is a separate, approved step (see below). <a href=\"/entries/langfuse/\">Langfuse</a> keeps the trace, so each proposal can be checked against the records it came from.</li></ol>\n<h2>Keep it read-only</h2>\n<p>Each of the three servers handles &quot;read-only&quot; differently, so set it up at both ends: in the credential, and in the harness.</p>\n<ul><li><strong>Xero: scopes.</strong> The server has no read-only flag; on a test run it listed 51 tools, 25 of which create, update, add, approve, revert or delete. Without <code>XERO_SCOPES</code>, it asks Xero for its default scope list, which includes read-write scopes such as <code>accounting.invoices</code> and <code>accounting.payments</code>. When <code>XERO_SCOPES</code> is set, it requests exactly those scopes and nothing else. The server&#39;s README lists the <code>.read</code> versions of the invoice, payment, bank transaction and report scopes. Create the Custom Connection with read scopes only and set <code>XERO_SCOPES</code> to the same list. Xero&#39;s FAQ says a call that needs a scope you haven&#39;t been granted gets a 401 with an insufficient-scope <code>WWW-Authenticate</code> header. Test with a Demo Company first.</li><li><strong>Stripe: agent key and approval rules.</strong> Stripe recommends OAuth for interactive clients. For an unattended agent, use an agent-tagged restricted key with read permissions on the resources you need. From December 2026, the MCP server stops accepting full-access secret keys and restricted keys without the Agent tag. Agent-tagged keys are subject to approval rules, and Stripe adds default rules for refunds, subscription cancellations and outbound payments. You can add rules (approve or block) for payouts, credit notes, customer balance transactions and more.</li><li><strong>Paddle: tool filter and key permissions.</strong> The default <code>--tools</code> setting is <code>non-destructive</code>, and that is not read-only. On a test run, <code>non-destructive</code> loaded 63 tools, including <code>create_transaction</code>, <code>create_adjustment</code> (refunds and credits) and <code>create_subscription_charge</code>. <code>read-only</code> loaded 41 tools. Use <code>--tools=read-only</code> and give the API key only <code>.read</code> permissions. Note that <code>create_report</code> is not a read-only tool. If you want the agent to start new CSV exports, add <code>create_report</code> to an explicit comma-separated tool list (42 tools), or let a person create the report and have the agent read it.</li></ul>\n<p>In the harness, auto-approve only read tools and hide the write tools. MCP tools are named <code>mcp__&lt;server&gt;__&lt;tool&gt;</code>. Allow rules accept a glob after the server name, and deny rules both block calls and remove the tool definitions. In <code>dontAsk</code> mode, any call that isn&#39;t approved is denied instead of prompting.</p>\n<pre><code class=\"language-python\">import os\nfrom claude_agent_sdk import ClaudeAgentOptions\n\n# Read-only Xero scopes. Give the Custom Connection only these, too.\nXERO_READ_SCOPES = &quot; &quot;.join([\n    &quot;accounting.invoices.read&quot;, &quot;accounting.payments.read&quot;,\n    &quot;accounting.banktransactions.read&quot;, &quot;accounting.contacts.read&quot;,\n    &quot;accounting.settings.read&quot;, &quot;accounting.reports.profitandloss.read&quot;,\n    &quot;accounting.reports.trialbalance.read&quot;,\n])\n\noptions = ClaudeAgentOptions(\n    mcp_servers={\n        &quot;xero&quot;: {\n            &quot;command&quot;: &quot;npx&quot;,\n            &quot;args&quot;: [&quot;-y&quot;, &quot;@xeroapi/xero-mcp-server@0.0.17&quot;],\n            &quot;env&quot;: {\n                &quot;XERO_CLIENT_ID&quot;: os.environ[&quot;XERO_CLIENT_ID&quot;],\n                &quot;XERO_CLIENT_SECRET&quot;: os.environ[&quot;XERO_CLIENT_SECRET&quot;],\n                &quot;XERO_SCOPES&quot;: XERO_READ_SCOPES,\n            },\n        },\n        &quot;stripe&quot;: {\n            &quot;type&quot;: &quot;http&quot;,\n            &quot;url&quot;: &quot;https://mcp.stripe.com&quot;,\n            # An agent-tagged restricted key with read permissions only\n            &quot;headers&quot;: {&quot;Authorization&quot;: f&quot;Bearer {os.environ[&#39;STRIPE_AGENT_KEY&#39;]}&quot;},\n        },\n    },\n    allowed_tools=[\n        &quot;mcp__xero__list-*&quot;, &quot;mcp__xero__get-*&quot;,\n        &quot;mcp__stripe__stripe_api_search&quot;, &quot;mcp__stripe__stripe_api_details&quot;,\n        &quot;mcp__stripe__stripe_api_read&quot;,\n    ],\n    disallowed_tools=[\n        &quot;mcp__xero__create-*&quot;, &quot;mcp__xero__update-*&quot;, &quot;mcp__xero__delete-*&quot;,\n        &quot;mcp__xero__approve-*&quot;, &quot;mcp__xero__revert-*&quot;, &quot;mcp__xero__add-*&quot;,\n        &quot;mcp__stripe__stripe_api_write&quot;, &quot;mcp__stripe__send_money&quot;,\n        &quot;mcp__stripe__transfer_money&quot;, &quot;mcp__stripe__convert_currency&quot;,\n    ],\n    permission_mode=&quot;dontAsk&quot;,\n)</code></pre>\n<p>For Paddle, add a server running <code>npx -y @paddle/paddle-mcp@0.1.6 --tools=read-only</code> with <code>PADDLE_API_KEY</code> and <code>PADDLE_ENVIRONMENT</code> in its <code>env</code>. Every tool it loads is then read-only, so <code>mcp__paddle__*</code> can go in the allowlist.</p>\n<h2>Writes: drafts first, and a human for each one</h2>\n<p>When the report is accepted, the fixes can be written, but not by the same unattended run. Use a second configuration with write scopes, and leave write tools out of <code>allowed_tools</code>, so each call reaches your <code>canUseTool</code> callback for a person to approve. That only works in <code>default</code> mode; <code>dontAsk</code> denies the call instead. Know what each Xero write does. In version 0.0.17 of the server:</p>\n<ul><li><code>create-invoice</code>, <code>create-credit-note</code> and <code>create-quote</code> always create <strong>drafts</strong>, and <code>create-manual-journal</code> defaults to draft. An accountant can review and approve these in Xero.</li><li><code>create-bank-transaction</code> creates the transaction as <strong>authorised</strong>, and <code>create-payment</code> records a payment against an authorised invoice straight away. Treat these as posting entries.</li><li>The server has no tool for bank statement lines or for reconciling them. Matching a statement line in Xero stays a job for a person in Xero itself.</li></ul>\n<p>On the Stripe side, writes go through <code>stripe_api_write</code>. Stripe can require human confirmation in the Dashboard for some actions, such as refunds, and your own approval rules can add others or block them outright.</p>\n<h2>Match payouts outside the model</h2>\n<p>The snippet below matches Stripe payouts to Xero bank transactions that receive money. A match needs the exact amount and an arrival date within a few days. Anything with zero or several candidates is sent to a person instead of being guessed. Stripe amounts are integers in the currency&#39;s minor unit, so it divides by 100. That is only right for two-decimal currencies; <a href=\"https://docs.stripe.com/currencies#zero-decimal\">zero-decimal currencies</a> such as JPY need no division.</p>\n<pre><code class=\"language-python\">from decimal import Decimal\n\ndef match_payouts(payouts, bank_lines, days=3):\n    &quot;&quot;&quot;payouts: Stripe payout dicts (amount in minor units, arrival_date as a date).\n    bank_lines: Xero RECEIVE bank transactions as dicts with total (Decimal), date and id.\n    Returns (matches, unmatched_payouts, unmatched_lines). Exact amount, date within +-days.&quot;&quot;&quot;\n    free = list(bank_lines)\n    matches, missing = [], []\n    for p in sorted(payouts, key=lambda p: p[&quot;arrival_date&quot;]):\n        want = Decimal(p[&quot;amount&quot;]) / 100  # two-decimal currencies only\n        hits = [b for b in free if b[&quot;total&quot;] == want\n                and abs((b[&quot;date&quot;] - p[&quot;arrival_date&quot;]).days) &lt;= days]\n        if len(hits) == 1:\n            matches.append((p[&quot;id&quot;], hits[0][&quot;id&quot;]))\n            free.remove(hits[0])\n        else:\n            missing.append((p[&quot;id&quot;], f&quot;{len(hits)} candidate lines&quot;))\n    return matches, missing, [b[&quot;id&quot;] for b in free]</code></pre>\n<p>For each payout that matches, check the next level down: the payout&#39;s balance transactions against the Xero invoices and payments for the same customers. That is where unbooked fees and refunds show up.</p>\n<p>Xero&#39;s <code>list-bank-transactions</code> tool returns 10 transactions per page as text, newest first. A busy month takes many calls, and every result passes through the model&#39;s context. Have the agent page through once, write what it reads to a file, and do the rest of the work in the sandbox.</p>\n<h2>Test before you connect live books</h2>\n<ul><li><strong>Xero:</strong> start with the Demo Company, as Xero&#39;s README recommends. It comes with sample data and can be reset.</li><li><strong>Stripe:</strong> use a sandbox. OAuth consent lets you grant sandboxes and live accounts separate permissions.</li><li><strong>Paddle:</strong> set <code>PADDLE_ENVIRONMENT=sandbox</code> with a sandbox key.</li><li>Keep a month you have already closed by hand as a fixed test case. Rerun the agent on it whenever you change the prompt, the model or a server version, and compare its list of differences with the one your accountant produced.</li></ul>\n<h2>Variations</h2>\n<ul><li><strong>Xero&#39;s hosted server.</strong> On 2026-10-15 Xero launched a Developer Remote MCP with 39 tools across invoicing, accounting, reporting and payments, for commercial apps on the Core tier or above. The local server above needs no app tier, only a Custom Connection or a bearer token.</li><li><strong>Bank data.</strong> The <a href=\"/entries/plaid-mcp/\">Plaid Dashboard MCP</a> is for diagnosing a Plaid integration (Items, Link conversion, usage), not for reading account transactions, so it is not a bank feed for this stack.</li><li><strong>Other processors.</strong> Square, PayPal, Razorpay and Adyen have their own MCP servers; the <a href=\"/compare/payment-processor-mcp-servers/\">payment processor MCP servers comparison</a> covers what each can read and write.</li><li><strong>Agents that spend money</strong> are a different stack: see <a href=\"/stacks/agent-that-can-buy-things/\">Agent that can buy things</a>.</li></ul>"
}
