- engineering blog
- Product
8 min read
Building the Attio MCP server
)
James Mulholland Senior Product Engineer
An MCP server has quickly become one of the most requested features for every tech company.
MCP is not only new, but ever-changing. With such conditions, it can be difficult to determine best practices. However, with lots of experimentation, thoughtfulness, and user feedback, we’re now confident that we have built one of the best MCP servers available.
We’ve seen our MCP power autonomous, end-to-end migrations into Attio from other products. Agents now run 24/7, scouring Attio and multiple other data sources for issues in the pipeline. And users can implement automations faster as they switch from hand-wired API calls to high-level AI delegation.
Here’s everything we learned about how to build a great MCP that makes these use cases possible.
First-class MCP servers
The obvious approach to building an MCP server is to wrap a pre-existing API. There are many services that will take an OpenAPI spec, auto-generate some tools, and present you with a functional MCP server.
While experiments with this approach did quickly get us to something working, we found it held us back from producing the best possible results.
Public APIs and MCPs have different users. The user of a public API is a programmer, whether human or AI. The code they write needs to be reliable, resilient to errors, and maintainable. It's used to power well-defined, long-lived integration workflows, and may operate on very large datasets. The user of an MCP server is an autonomous AI agent. Rather than running pre-defined workflows, agents ask questions and take actions on the fly.
These two users produce very different design requirements.
Take pagination for example. A big use case for Attio's public API is ETL-style workflows where an engineer might need to query millions of records. If you encourage an agent to fetch this data, you'll quickly hit context limits. Instead, we designed tools for agents to run aggregated queries across large datasets, use fuzzy and semantic search, and quickly get the data they need for autonomous task execution.
We also found that fewer, richer tools tend to outperform many atomic ones. When we built our comments tool, we initially considered an approach where agents could first list top-level comments, then fetch thread replies in separate tool calls if they needed to. In practice, we found agents almost always needed to make multiple tool calls. Each additional tool call costs tokens and adds latency. It's another point where the agent can go off the rails, and another tool to search through. An updated approach where we returned nested comments for all threads provided faster and more reliable.
Another downside of simple API wrapping is that it fails to help with one of the most important parts of tool design: descriptions. We paid great care not only to which tools we provided, but also to how they were described and documented for the AI. Tool descriptions are the prompt for your MCP. It’s important to get this right.
Finally, the approach to breaking changes can also differ dramatically between public APIs and MCPs. Public APIs cannot break. Code is written around them that expects certain behavior. But MCP tools are far more flexible. If you want to change an API response, you can't. If you want to change a tool, it's very easy to iterate. The agent won’t care. Coupling your MCP closely to an API can hold you back. First-class building for MCP can set you free.
Context management: every token counts
When you return data from a regular API, you can afford to be generous: send back every property and let the caller decide what they need. In an MCP server, every token matters. Return too much data and you hit context limits quickly and burn through tokens. We had to be much more deliberate about what our tools returned and how they formatted it.
Beyond being mindful about which properties our agents needed, the biggest win was rendering responses in TOON (Token-Oriented Object Notation). TOON is a compact encoding designed specifically for language models. It uses YAML-style indentation for nested objects and a CSV-like tabular layout for arrays, and declares field names once. Data can be streamed in rows, another advantage over JSON. The result is the same information in a fraction of the tokens.
{
"users": [
{ "id": 1, "name": "Alice", "role": "admin" },
{ "id": 2, "name": "Bob", "role": "user" }
]
}
users[2]{id,name,role}:
1,Alice,admin
2,Bob,user
We were surprised at how well models understand TOON. We expected to spend time prompting agents on TOON format, but agents handled it without any problems from day one, no explanation needed.
Authentication: the hardest part of MCP
The most significant challenge we encountered was authentication. Simpler MCP servers can skip authentication or use a simple access token approach. For Attio, that wasn't an option. We want users to authenticate as themselves so we can apply permissions correctly. We also wanted our flow to be seamless; clunky token configuration was out of the question.
The natural solution to such problems is OAuth, and Attio already had OAuth flows. The catch was that MCP clients expect specific OAuth extensions that most services do not support by default. In a typical OAuth flow, an integration developer registers their client ahead of time and receives a client ID and a client secret. They can then configure these values in their integration somewhere to allow users to connect.
MCP doesn’t follow this manual registration flow. Instead, MCP clients utilize dynamic OAuth flows where clients register themselves, no developer config required. The two standards supported are client ID metadata documents and dynamic client registration. Client ID metadata documents allow clients to register themselves by providing a URL which points to a document containing the necessary metadata. Dynamic client registration includes a separate registration step ahead of the main authorization flow.
We evaluated both approaches and ultimately decided upon dynamic client registration. This approach has been around for longer and thus has broader support. Plus, client ID metadata documents require publicly available URLs, which would be unsuitable for many local clients that don't exist as a server on the public internet.
The payoff for this approach has been clear. Users are able to setup the MCP with minimal configuration, often by just clicking through a simple, familiar flow. When they authenticate, they are authenticated as themselves, with Attio’s powerful permissions system applied as expected. Everything works as it should.
Using AI to build the MCP
The Attio MCP wasn't just built for AI. It was built with AI throughout the entire process.
User testing with agents
AI users have a nice side-effect: user testing is trivial. If you have a question about schema design, tool descriptions, or functionality, the feedback is but a prompt away.
We iterated on tool design with agents constantly. Before writing any code, we'd describe the requirements to an LLM in general terms and ask it to propose some design options. Then we'd open a fresh context window where the LLM had no prior knowledge of what we were building, give it the designs and some test prompts, and see how it did. Did it pick the right tool? Did it understand the parameters? Did it format the input correctly?
For example, we spotted that agents were using kebab case instead of snake case on some tools, so we added some examples to our descriptions. Internally, we refer to items in an Attio list as ‘entries’. One tool was originally called ‘create entry’ but this terminology proved confusing. We updated the tool name to ‘add record to a list’.
Any friction we spotted was immediately fed back into the designs before testing again. This loop was fast and caught issues we'd have otherwise only found after shipping, if at all. With the help of early users, we continued this process, tweaking tools based on how agents actually used them in the wild.
Building with skills
Once we'd hand-built our first tools, we settled on the core patterns as a team and encoded that knowledge into Claude Code skills. We built scripts to generate boilerplate code (again, with AI), referenced gold-standard example implementations, and documented best practices. This enabled us to go from user request to functional tool in literally minutes. Looking forward, our skills will also serve as clear documentation to the future builders and maintainers of the MCP.
Testing with harnesses
A more advanced technique involved constructing a harness around the MCP which allowed Claude to autonomously test its own work. With Claude Code being a MCP client itself, we were able to wire up the in-development MCP to the implementing agent. Claude was able to call the tool it was building directly and verify that the results were as expected. MCP tools could also be used in development to rapidly seed test data for both human and AI testing.
On top of this, test-writing is one of the things that agents do best. We were able to supplement manual testing with AI-generated unit and integration tests, further increasing our confidence and reducing the occurrence of bugs.
Learning and evolving
The skills required to build MCP tools share many similarities with those required to build traditional APIs. You must consider the details, think of your user, and test things thoroughly.
The details are different, however. APIs are structured and precise. Agents call tools more conversationally. On top of this, the landscape is evolving at a rapid pace. For example, context consumption from a high number of tools with lengthy descriptions used to cause a problem. Now, tools like Claude Code implement tool search layers that have eliminated this as a problem. Good advice today will likely be bad advice tomorrow.