How I Built an MCP Server That Lets Claude Work with Exact Online | Forrict Skip to main content
AI Build log

How I Built an MCP Server That Lets Claude Work with Exact Online

Fons Biemans
How I Built an MCP Server That Lets Claude Work with Exact Online
A build log on my own MCP server for Exact Online: 65 tools, sign-in via OAuth, running on AWS Lambda and safety nets so Claude never books anything on its own. With the real code and the lessons learned.

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:

DateMilestone
8 July 2026First version: OAuth, the API client and 28 tools for CRM, financials, sales, purchasing and logistics
24 September 202628 more tools, “insight” tools that combine several API calls into one answer, and safety nets for write actions
30 September 2026Every 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.

AreaWhat Claude can do with it
CRMFind, create and update customers and contacts
FinancialG/L accounts, journals, transaction lines, open receivables and payables, aging analysis, financial periods
ReportingProfit and loss and balance sheet, budgets, bank entries, payments, VAT codes
Sales and purchasingSales invoices and orders, purchase entries and orders
OtherQuotations, opportunities, documents, the digital mailbox, projects and hours, employees, subscriptions, items and stock
InsightsOne 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 queryexact_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

Design: Claude speaks MCP with the server, the server speaks with the Exact Online REST API, either locally over stdio or in AWS Lambda behind its own OAuth server

LocalAWS Lambda
ForClaude Desktop and Claude Code on your own computerClaude on claude.ai and Claude Code, with nothing running on your computer
ConnectionstdioMCP Streamable HTTP via a Lambda Function URL
TokensA file on your computer (readable by you only)AWS Secrets Manager
Sign-inA one-time npm run authYou 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:

  1. Claude calls /mcp without a token and gets a 401. The metadata tells it where to sign in.
  2. Claude sends you to /authorize, which forwards you to the Exact Online sign-in, including Exact’s two-step verification.
  3. After sign-in the server checks that you are the Exact user who is allowed in. If you are someone else, you get a 403 and nothing is stored.
  4. 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

F

Fons Biemans

AWS expert and consultant at Forrict, specializing in cloud architecture and AWS best practices for Dutch businesses.

Tags

Exact Online MCP Model Context Protocol AWS Lambda Claude

Stay in the loop

Get AWS insights and tips delivered to your inbox.

Stay in the loop

Get AWS insights and tips delivered to your inbox.

Related Articles

What Claude Does for My Bookkeeping in Exact Online

What Claude Does for My Bookkeeping in Exact Online

How I use Claude for the bookkeeping of three companies in Exact Online: asking questions, a group report with a forecast, tracking down missing invoices, closing a quarter and preparing sales invoices. With an example report and the rules that make sure Claude never books anything on its own.

Read more
My House Now Decides for Itself When It Uses Power

My House Now Decides for Itself When It Uses Power

A build log on how I use Home Assistant, EMHASS and Claude Code to run my home battery, water heater, dishwasher and car at the cheapest moment, every quarter hour. With the real code, the safety nets and the results from September 2026.

Read more