In part 1 I showed what Claude does for my bookkeeping: a group report, tracking down missing invoices, closing a quarter and preparing sales invoices. That works because Claude can look straight into Exact Online, through an MCP server I built for it myself. This part is about that server: how it fits together and what gave me trouble along the way. The full code is public on GitHub.
What is MCP again?
The Model Context Protocol (MCP) is an open standard that lets an AI assistant like Claude call tools in other systems. An MCP server hands over a list of tools, each with a name, a description and the parameters it expects. For every question, Claude picks the tool that fits.
So my server is a translator. Towards Claude it speaks MCP, towards Exact it speaks the Exact Online REST API.
From 28 to 65 tools
I built the server together with Claude, in TypeScript with the official MCP SDK. The first version was ready in July, and two more rounds followed:
| Date | Milestone |
|---|---|
| 8 July 2026 | First version: OAuth, the API client and 28 tools for CRM, financials, sales, purchasing and logistics |
| 24 September 2026 | 28 more tools, “insight” tools that combine several API calls into one answer, and safety nets for write actions |
| 30 September 2026 | Every write tool in preview mode by default, and choosing an administration by name instead of by number |
It now has 65 tools. 56 of them only read, 9 create or change something.
| Area | What Claude can do with it |
|---|---|
| CRM | Find, create and update customers and contacts |
| Financial | G/L accounts, journals, transaction lines, open receivables and payables, aging analysis, financial periods |
| Reporting | Profit and loss and balance sheet, budgets, bank entries, payments, VAT codes |
| Sales and purchasing | Sales invoices and orders, purchase entries and orders |
| Other | Quotations, opportunities, documents, the digital mailbox, projects and hours, employees, subscriptions, items and stock |
| Insights | One call for a P&L summary, a check for missing bookings, the status of the bank feeds, the mailbox with attachments, suggestions for matching payments to invoices, and a consolidated P&L across several administrations |
| Free query | exact_odata_query for any endpoint that does not have its own tool yet |
The insight tools are the ones I use most. A question like “am I missing any bookings?” would otherwise take six separate calls per administration. Now one tool puts negative receivables and payables, open items on suspense accounts and the number of invoices per month side by side. A gap jumps out right away.
Two ways to run it
| Local | AWS Lambda | |
|---|---|---|
| For | Claude Desktop and Claude Code on your own computer | Claude on claude.ai and Claude Code, with nothing running on your computer |
| Connection | stdio | MCP Streamable HTTP via a Lambda Function URL |
| Tokens | A file on your computer (readable by you only) | AWS Secrets Manager |
| Sign-in | A one-time npm run auth | You sign in with your Exact account when you connect the connector |
I use the Lambda version myself, as a custom connector in Claude. That way nothing has to run on my own computer. The whole environment is one AWS CDK stack. It holds a Node.js 22 Lambda, one secret, CloudWatch alarms for failed sign-ins and errors, and a weekly EventBridge rule that refreshes the Exact token. That rule keeps the token from expiring when I go a while without asking Claude anything about Exact.
Signing in: OAuth twice
A server on the internet that can reach your accounting has to be locked down properly. The MCP specification prescribes OAuth 2.1 for this. So my Lambda contains a small OAuth server of its own, which in turn hands the sign-in over to Exact Online. Hence: OAuth twice.
Connecting works like this:
- Claude calls
/mcpwithout a token and gets a401. The metadata tells it where to sign in. - Claude sends you to
/authorize, which forwards you to the Exact Online sign-in, including Exact’s two-step verification. - After sign-in the server checks that you are the Exact user who is allowed in. If you are someone else, you get a
403and nothing is stored. - Claude exchanges the code (with client secret and PKCE) for a token that is valid for one hour and a 30-day refresh token.
There is only one pre-registered client, and the server accepts only Claude’s callback addresses. If I want to revoke every issued token at once, I change the signing key in the secret.
Writing safely: Claude never books anything on its own
Reading is harmless, writing is not. So the same rules apply to all 9 write tools.
The most important one is preview mode. Every write tool defaults to dry_run: true and then only shows what it would send to Exact. It only really happens with dry_run=false. On top of that, the server only creates drafts. General journal entries, purchase entries and sales invoices arrive in Exact as drafts, and I finalise them myself, in Exact.
Every tool also gets an MCP annotation that says whether it only reads. That tells Claude when to ask for confirmation. One setting puts the server in read-only mode, and then every write tool disappears from the list. Every write action becomes a line in the logs, in CloudWatch on Lambda. The server never logs query strings, request bodies or tokens.
The annotations live in one central place. Every tool whose name starts with exact_create_ or exact_update_ counts as a write tool:
const writes = WRITE_TOOL_RE.test(name);
if (writes && readOnly) return undefined; // read-only mode: do not register the tool
return register(name, {
...config,
annotations: {
readOnlyHint: !writes,
destructiveHint: false,
idempotentHint: !writes,
openWorldHint: true,
...config.annotations,
},
}, cb);
An annotation like that is only a hint. The MCP specification itself says a client should not trust it blindly. The real protection is the preview mode and the fact that only drafts get created.
What I learned along the way
The most important lesson was about tokens. An Exact access token is valid for 10 minutes. Every refresh gives you a new refresh token, and the old one expires immediately. Two refreshes at the same time, and you are locked out.
So the server saves the new token before anyone uses the access token, and refreshes wait for each other. If two Lambdas run at the same time, the server reads the store again and adopts the other one’s token. A missed refresh means signing in again, a double refresh means you are locked out. This is the piece of code it all hinges on:
private async doRefresh(): Promise<TokenSet> {
// Another process may have rotated the tokens already.
let current = await this.loadFromStore();
if (this.isFresh(current)) return current;
let next: TokenSet;
try {
next = await this.refreshWith(current);
} catch (err) {
// Lost a race: another process already used our refresh token.
if ((err as OAuthError).code !== "invalid_grant") throw err;
const latest = await this.loadFromStore();
if (latest.refresh_token === current.refresh_token) throw err;
if (this.isFresh(latest)) return latest;
current = latest;
next = await this.refreshWith(current);
}
// Save the new single-use refresh token first, use it after.
await this.store.save(next);
return next;
}
The Exact API had a few other quirks. When I sent $top, some endpoints, such as financial/ReportingBalance, returned empty lists. The server no longer forwards $top. It paginates itself via the __next links and warns when a result is empty, so at least it no longer fails silently.
Exact also limits the number of calls per minute and per day. On a 429 the server waits as long as Retry-After says and tries once more. Every list shows how many calls are left this minute.
And Exact uses old-style OData, with dates like /Date(1735689600000)/. The server converts those to plain ISO dates and strips __metadata and empty fields from every response. That saves Claude a lot of text to read through.
Then two smaller things. In Exact you choose an administration by number. With my server you can also use its name, or part of it. The server fetches the list of administrations once and remembers it. And the redirect field in the Exact App Center cuts off long addresses. For me it happened after 45 characters, so check carefully what is really in that field after saving.
What I take away from it
The 65 tools were the easy part. Most of the time went into everything around them: signing in, tokens that work only once, API limits and the safety nets around writing. That is not specific to Exact. You can connect any system with an API to Claude this way, as long as you think carefully up front about who gets access and what is allowed to change.
Want to run the server yourself? The repository on GitHub explains step by step how to register an app in the Exact App Center and start the server, on your own computer or on AWS Lambda. The code is licensed under the PolyForm Small Business License. Companies with fewer than 100 people and less than 1 million dollars in annual revenue may use it. Larger organisations can get a commercial license from me.
What I use it for in practice is in part 1.
Do you work with Exact Online and want a connection like this too? I help SMEs automate their workflows and train teams that want to build with AI. Send me a message and we can look at what would work for you.
Sources
- The server’s code: Forrict/mcp-exactonline on GitHub
- Model Context Protocol: specification, authorization and tools and annotations, TypeScript SDK
- Exact Online: REST API resources, obtain new access tokens, API limits
- Token lifetime and single-use refresh tokens, explained by a moderator: Thinkwise Community
- Anthropic: add a custom connector in Claude, MCP in Claude Code
- AWS: AWS CDK, Lambda Function URLs
Fons Biemans
AWS expert and consultant at Forrict, specializing in cloud architecture and AWS best practices for Dutch businesses.