~/rena
← blog
MCPIntegrationsAI EngineeringArchitecture

MCP and the End of Bespoke Tool Glue

For two years every AI integration I built was hand-rolled glue. Standardized tool protocols changed the economics of that work — but not in the way most write-ups suggest.

RRena Thomas·

For about two years, every AI integration I built was bespoke glue. A tool schema here, a webhook receiver there, an auth dance specific to one vendor's API, a serialization layer nobody else would ever reuse. Multiply by every system the agent needed to touch.

Standardized tool protocols — the Model Context Protocol most visibly — changed the economics of that. I've now built and consumed enough of them to have opinions that aren't just enthusiasm.

What the standard actually fixes

The thing it fixes is not "connecting an LLM to a tool." That was always easy. Function calling has been a solved problem in the narrow sense since 2023.

What it fixes is the N×M problem. Before, every (model host, data source) pair needed its own integration. Your agent framework spoke to Postgres one way, your teammate's spoke to it another way, and the CRM connector one team built was unusable by anyone else because it was welded to their orchestration layer.

A protocol turns that into N + M. You write one server per system you own. Every client that speaks the protocol gets it for free. That's not a novel insight — it's the same argument as LSP for editors, or ODBC for databases — but it took the industry a while to arrive at it, and the payoff has been real.

Concretely: on a recent engagement, the internal knowledge-base connector I wrote for an agent got picked up by two other teams within a month, with zero coordination from me. That never happened with hand-rolled glue.

What it doesn't fix

Here's where I part ways with a lot of the writing on this.

It doesn't fix your data. A protocol standardizes transport and description. It does nothing about the fact that your CRM has four fields that all sort of mean "customer status" and the agent can't tell which one is authoritative. Standardized access to a mess is faster access to a mess.

It doesn't fix tool proliferation. This one bit me. Once connecting a system is cheap, people connect everything. I've seen an agent handed sixty-odd tools across a dozen servers. Model performance falls off a cliff — not because the model is bad, but because tool selection becomes a retrieval problem in disguise and nobody treated it as one.

My rule now: an agent gets at most 15–20 tools in context at once. Beyond that, you need tiering — a router that selects a tool subset by task before the working agent sees anything:

// Don't hand the agent everything you can connect to.
const relevant = await selectToolset({
  task: session.intent,
  available: registry.list(),      // may be 60+
  limit: 15,
})

const agent = createAgent({ tools: relevant, state: session.state })

Somebody will tell you the next model generation makes this unnecessary. It has been said about every generation so far. It's been directionally true and never fully true.

It doesn't fix authorization. This is the big one, and I think the industry is still underrating it. A tool server that runs with broad service-account credentials and serves an agent that reads untrusted input is a confused deputy waiting to happen. The protocol gives you a clean way to expose a capability. It does not decide who is allowed to invoke it, on whose behalf, with what scope. That's your job, and it's a real design problem, not a config flag.

I'll write about that properly — it deserves its own post.

How I structure servers now

Some patterns that have held up across a handful of these:

One server per trust boundary, not per system. If two systems share the same credentials and the same blast radius, one server is fine. If reading from the data warehouse is safe and writing to production billing isn't, those are two servers even if the same team owns both. It makes the authorization story legible.

Tools describe outcomes, not endpoints. The temptation is to map one tool per REST route. Resist it. GET /patients/:id, GET /patients/:id/encounters, GET /encounters/:id/notes becomes one getPatientSummary tool that does the joins server-side. Models compose poorly across many small calls, and every hop is latency plus a chance to go off the rails.

Descriptions are prompt engineering. The tool description is part of your prompt. Treat it like one — version it, eval it, and write it for a reader who has no other context. "Returns the patient record" is useless. "Returns demographics, active medications, and the three most recent encounters. Does not include billing. Use when the caller needs clinical context before scheduling." is a tool the model uses correctly.

Return errors the model can act on. A 500 with an opaque body makes the agent retry blindly. { error: "date_out_of_range", detail: "start must be within 90 days of today", retryable: true } makes it correct itself on the next turn. Same principle as validating tool inputs at the boundary — the model is a client, so give it a decent API.

The integration work didn't disappear

I want to be careful not to overclaim. The glue work moved; it didn't vanish.

What used to be "write a connector" is now "write a server, decide its trust boundary, design its tool surface, write descriptions that work as prompts, and figure out how it composes with the twelve other servers this agent can reach." That's still integration engineering. It's just integration engineering with a shared vocabulary and reusable output.

The honest summary: the protocol made the mechanical half of AI integration close to free, and left the design half exactly as hard as it was. Which is fine. The design half was always the interesting part.


Related: the multi-provider voice abstraction I built solved the same N×M problem in a narrower domain, before a standard existed. Having done both, I'd take the standard every time.