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
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
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
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
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
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.