Skip to main content

How we built Seapoint's MCP

Here's how we built the initial Seapoint MCP, what made it slow, the different approaches we explored, and how we measured the improvements along the way.

Doruk Güneş

Founding Engineer

How we built Seapoint's MCP

Seapoint's MCP server lets you connect your AI agents to your Seapoint account and ask questions about your company's finances in plain English. Things like "What's our runway?", "Who were our biggest suppliers this quarter?" or "Which transactions haven't synced to Xero yet?"

Our first version could answer these questions correctly, but it wasn't particularly fast or efficient. For example, asking about your runway could take around four and a half minutes and bring over 1 MB of JSON into the conversation. Today, the same question takes less than a minute and uses around 36 KB of data.

In this post, I'll share how we built the initial version, what made it slow, the different approaches we explored, and how we measured the improvements along the way.

Step one: start out simple

When we started in May, we did what most companies do when they build their first MCP server. We looked at what others had already shipped and followed the pattern most had settled on: expose your Public API convention as tools.

So version one had a handful of list_* tools, such as list_transactions and list_invoices. Each tool took a few flat filters like a date range, a direction or an amount range, and returned a page of rows, starting with 25 by default and going up to 100.

Looking back, it was the most natural place to start for two reasons:

  • Everything already existed. Our web app already has these entities, the services that read them and the permission rules that decide who can see what.
  • It follows a familiar convention. The tools look like a public API, so if you have used one before, you already know what list_transactions with date_gte and date_lte does.

It was ready for internal testing in June, and for simple lookups like "show me my latest transactions" it worked really well.

Step two: ask the right questions

However, most finance questions aren’t lookups, but are totals, comparisons and what-ifs. Using the model we currently had, we could only find answers by finding the total of all the rows.

A good example is "How will our runway change if we hire one more engineer?". With the list tools, that meant fetching every outgoing transaction in the window and adding them up.

In August I timed how long it took twelve common questions to run end to end, and the result completely changed how I looked at the problem.

Our server spent 2.4 seconds of those seven minutes doing actual work, and successful MCP requests in production take 44 ms on average. The other 99% of the time went into the model reading pages and the loop around it.

When we looked into why it was taking so long, here’s what we found:

  • Pages can't run in parallel. Each page needs the cursor from the previous one, so 17 pages means 17 trips through the model, one after another.
  • Pages were too big for the client. A 100-row page was about 76 KB, which is more than Claude Code passes to the model at once. It saved each page to a file instead, and the model needed an extra turn to read it back.
  • The model retyped the cursor. Every page ended with a 300-character cursor that the model had to copy into the next call.
  • Answers could be wrong. If the context fills up before the rows run out, we increases the chance of hallucination.

With the list tool taking far too long to retrieve answers to more nuanced questions, we knew we needed to switch up our approach.

Step three: find the right approach

So instead of the list tool, we tried the following approaches:

MCP approaches
OptionWhat it looks likeWhy it didn't fit us
More filters, more toolsAdd get_burn, get_spend_by_category and so on.Every new question would need a new tool, making it difficult to manage as the list grows.
Copy the data, then query the copyLoad transactions into an in-memory SQLite database for each session.An improvement for continuous querying, but data staleness risks skewing results.
A dedicated query layerStream every change into pre-joined tables built for querying.This options works best on a much larger scale that we’re not at yet. Our approach keeps the door open to move there later.
A small query languageThe model describes what it wants, and our server builds the SQL and does the maths.Our choice.

After settling on a small query language, we replaced the five list_* tools with five query_* tools. Each one takes a JSON filter, where, built from names the model already understands, like direction, date and amount.

If you add an aggregate block, the same tool returns totals instead of rows. Here's "Who were our biggest suppliers this quarter?":

In a single call ten groups come back, one per vendor, ranked over every matching transaction in your reporting currency at each transaction's own day's rate. Since our AI enrichment matches bank transactions to vendors, each group is a real supplier rather than a raw bank description.

A few rules shaped the design:

  • It exposes Seapoint concepts, not tables. Each tool has a registry that maps a public field name to one SQL expression we wrote. The model can combine those names however it likes, but it can't reach anything the registry doesn't list.
  • Rows and totals come from the same tool. A separate totals tool could drift from the rows it summarises, and then the total wouldn't match the list.

What I like most is the clear separation between the interface and the backend. The model only interacts with the filter language, while everything behind it remains under our control. This gives us the flexibility to iterate, experiment, and evolve the backend without breaking our contract with clients.

Step four: measure the results

I didn't want to tune things based on feel, so the first thing I built was a small benchmark.

It works like this:

  1. Compose twelve questions people are most likely to ask. Each one has its minimal correct sequence of tool calls written down, and the date windows are relative, so runs stay comparable.
  2. A/B test the same question. Run the same question against the list approach and the aggregate approach, and only count it if both give the same answer, so the difference is cost and not correctness.
  3. Time how long it takes to run end-to-end. Stamp the clock before and after a tool call to see which of the two approaches is the faster.

In one test run, I asked for vendor spend over the last twelve months, with both the list and aggregate approaches returning the same total across 808 transactions. The old tools took 10 calls, 20 model turns and 150 seconds, while the new ones took 4 calls, 3 turns and 25 seconds.

Here's the same suite before and after the switch:

Questions that were already a single call didn't get faster, and we didn't expect them to. What really changed was that the cost no longer grew with the data.

In a list tool, paging rows costs tokens for every transaction in that period. For an aggregate approach, answers are sized by the number of groups, so token cost stays flat.

We're still very early in this journey, and I expect the query language, resources, and how we measure performance to evolve as we learn more about how people are using the MCP.

But one thing I'm pretty confident about is that more and more interactions with software will happen through agents rather than traditional UIs. I think this shift will only accelerate, and making sure Seapoint can give those agents fast, reliable and accurate answers will become increasingly important.

Try it today

Get your full financial picture in ten minutes