All posts
by Dmitriy Ginzburg

Talk to your accounts: a FreeAgent MCP server on Cloudflare

We put the company's FreeAgent books behind a remote MCP server on Cloudflare Workers, guarded by Cloudflare Access. Cursor, Claude, ChatGPT and our own dashboard now query and post to the ledger through one URL, with per-person read/write permissions.

Our books live in FreeAgent. Until recently, asking a question about them meant opening the app, finding the right report and reading it. Now we ask the agent we already have open. "What did we spend on hosting in the last financial year?" "Which invoices are overdue and by how long?" "Post the journal for this month's depreciation." Cursor, Claude, ChatGPT and our own admin dashboard all talk to the same server, sign in as the same people, and get the same answers.

This post is about how that server is put together and why it took days rather than weeks. There is no source code here; the interesting part is the shape, not the lines.

What it can do

The server exposes 56 MCP tools over FreeAgent's API. Reports: profit and loss, balance sheet, trial balance, opening balances. Documents: invoices (including sending and marking paid), bills, expenses, credit notes. Banking: accounts, transactions and their explanations. Bookkeeping: journal sets, categories and nominal codes, with the server refusing any journal whose entries do not balance. Plus contacts, projects, estimates and timeslips read-only, and a guarded passthrough for the corners of the API we have not wrapped yet.

A built-in prompt teaches the model FreeAgent's conventions before it starts: resources are identified by URL, categories are nominal codes, dates are ISO, and a journal must sum to zero. That one page of context is the difference between an agent that fumbles and one that reads like an assistant who has used the product.

How it is built

Cloudflare Workers + Durable Objects

Cloudflare Workers + Durable Objects

The whole server is one Worker. A Durable Object holds the single company-level FreeAgent grant and refreshes it serially, so clients never see a token; a second one holds the write/delete policy so it can be changed at runtime without a deploy.

Cloudflare Access

Cloudflare Access

Zero Trust in front of the hostname. Humans sign in through Access's managed OAuth with a one-time PIN; scripts present a service token. The Worker only ever verifies the signed identity Access attaches to the request.

Model Context Protocol

Model Context Protocol

Streamable HTTP transport via Cloudflare's Agents SDK and the official TypeScript SDK. Every client that speaks MCP and OAuth connects with nothing more than the URL.

FreeAgent API

FreeAgent API

A clean REST API with long-lived OAuth refresh tokens, which is what makes a single company-level connection practical. Sandbox and production apps map to local development and the deployed Worker.

Convex + Next.js

Convex + Next.js

Our admin dashboard is itself an OAuth client of the server: it signs each administrator in, stores their sealed grant, and shows connection health and permission tier on an Integrations card. The Worker sees the real person, not a shared key.

The security model in a nutshell

  • Nothing reaches the Worker without a valid Cloudflare Access identity. There are no API keys in any chat client, no VPN, no allow-listed IPs.
  • FreeAgent's tokens never leave the Worker. Clients authenticate to Access; the Worker authenticates to FreeAgent. The two credentials never meet.
  • Permissions are per identity, not per client. The same person gets the same tier from Cursor, Claude or a script, and a lost laptop is handled by revoking that person in Access.
  • Owners manage the write list and the delete switch on a small page served by the Worker itself, so the trust boundary is one hostname.
  • Every tool call is logged with who, which tool, success and duration. Request and response bodies are not.

None of this is exotic. It is the same pattern any small business can use to expose an internal API to AI tools without handing out secrets.

Connecting a client

Cursor

Add the server URL under Settings → MCP. The first tool call opens the Access sign-in in a browser; the grant lasts 14 days.

Claude

Settings → Connectors → Add custom connector, paste the URL, leave client id and secret blank. Claude registers itself and asks for your one-time PIN. Works on web, desktop, mobile and Cowork.

ChatGPT

Enable Developer mode under Apps & Connectors, create a connector with the URL and OAuth authentication, sign in with the PIN. Web only; write tools need a Business or Enterprise workspace.

Scripts and headless agents

Send the Access service-token headers with each request. The Worker treats the token as its own identity, which an owner can grant write access like anyone else.

What we would tell someone starting today

Put the identity layer in front of the server rather than inside it. Cloudflare Access handles sign-in, OAuth client registration for every agent vendor, session lifetimes and revocation, and the Worker shrinks to a thin, honest translator that verifies one header. Keep the upstream grant in a single place, keep write permissions as data you can change at runtime, and log who did what. The MCP surface itself is the easy part.

If your business runs on a SaaS with a decent API and you would like your agents to work with it the same way, get in touch.