Skip to content

16 min read

What an MCP server decouples

On this page

The Model Context Protocol — MCP — is an open specification, published by Anthropic in November 2024, that fixes one wire format for the conversation between an application that wants a capability and a process that provides one: how the application asks what is on offer, how the answer is described, and how a call and its result travel. An MCP server is that second process, the one you run, exposing your capability over the protocol so that any client implementing the same protocol can connect to it, ask what it offers, and call it, without you shipping a line of code specific to that client. Most engineers meet the far end of this before they build one, as the mcpServers block in a coding assistant's or a desktop application's settings file, where four lines of JSON naming a filesystem, GitHub or Postgres server and a restart make a set of new tools appear in an application whose authors had never heard of them.

This chapter is about being the team on the other side of that block, and about what exactly it is that you have decoupled by standing there. The claim worth being careful with is narrow, because the broad version of it — that MCP makes tools better, faster or cheaper — is wrong in every particular. What the protocol changes is where a tool's definition lives and who is allowed to reach it, and the value of that change is a function of how many things need to reach it.

The multiplication that becomes an addition

Consider a capability owned by one backend team: a search_tickets function that queries a support database, ranks by recency and text relevance, and returns at most twenty rows carrying an id, a title, a status and a snippet. Inside their agent it is registered through the provider's tool-calling API with a name, a one-paragraph description and a JSON Schema — a machine-readable declaration of the arguments it accepts — for {query, status, limit}, and when the model emits a tool call the loop dispatches to the Python function. Now suppose three more things want it: an assistant built by another team on a different SDK, a desktop agent running on laptops with no network route into the VPC, and two engineers' editors. The query has not changed. What has changed is the number of things that need to reach it, and in that arrangement each one needs a piece of code somebody writes and somebody keeps alive.

It is worth pricing that concretely rather than leaving it as a feeling of drag. The adapter the second team needs is not enormous — an HTTP client with retries, a tool definition transcribed into their SDK's argument shape, a result formatter, and a test — call it 150 lines and three days including review. The desktop agent is worse because it has no route into the network, so somebody spends a week on an authenticated gateway before writing any of the same 150 lines. The editor extensions are a different runtime again. None of these are hard problems, and that is precisely the complaint: four teams each spend between three days and two weeks producing four slightly different transcriptions of one schema, and every one of those transcriptions is a copy that can fall out of date. Widen limit from 20 to 50 and the change is one line in the service and four tickets in four backlogs; the two that never get done become a support thread six months later about why ticket search "only ever returns twenty".

Why that cost is a multiplication rather than a sum is the step worth doing slowly, because it is what decides whether the problem grows or merely gets bigger. Each transcription is one consumer's copy of one capability's declaration, so it is indexed by a pair: which consumer, and which capability. Writing the desktop agent's copy of search_tickets makes the editor's copy no cheaper, because the two are different languages against different SDKs written by different people. Writing the desktop agent's copy of search_tickets makes that same agent's copy of a second tool no cheaper either, because a second tool is a second name, a second description and a second argument shape. Independent pairs, each costing its own days, is exactly the situation in which the total is the count of consumers times the count of capabilities rather than the two added together. Four consumers and one capability is four transcriptions; add two more tools next quarter and it becomes four times three rather than four plus two.

The shape of this problem is old and the fix is a known one. In 2016 Microsoft published the Language Server Protocol for exactly the same arithmetic: every editor that wanted Go To Definition in every language needed a bespoke integration, so M editors times N languages was M×N pieces of work, none of which was interesting. LSP made it M+N — each language ships one server, each editor ships one client, and neither side knows about the other. MCP is a direct descendant of that idea applied to models and tools, and the official architecture overview makes the lineage explicit.

Flowchart: agent → search; agent → create; chat app → search; chat app → link; desktop → create; editor → link.
Flowchart: agent → MCP server; chat app → MCP server; desktop agent → MCP server; editor → MCP server; MCP server → ticket DB.

The second picture is the whole claim. Every edge in the first diagram is code somebody wrote and somebody maintains; every edge in the second is configuration. That is the difference the protocol buys, and it is worth being precise that it buys nothing else — the database query, the ranking, the auth to the ticket system and the twenty-row cap are yours either way, sitting in one place instead of being reimplemented per consumer.

Both pictures are worth counting rather than glancing at, because the label on the first one prices a worse case than the drawing shows. Four clients against three tools is twelve possible pairings, and twelve is the figure in the label: it is what the work comes to if every client wants every tool. Fewer edges than that are drawn, because in this particular fleet the chat app never asked for create and the editor never asked for search. A real fleet usually sits somewhere below the ceiling the multiplication names. The ceiling is still the right thing to price, because every new tool and every new consumer moves the fleet towards it, and neither of those is a decision the owning team gets to make.

What a client gets without you writing anything for it

The mechanism that makes the addition work is discovery. A compliant client does not need to be told what a server can do, because it can ask. It opens a connection, negotiates, and sends a request for the tool list, and what comes back is a machine-readable declaration of every tool with its name, its human-readable description and its input schema:

json
{
  "tools": [
    {
      "name": "search_tickets",
      "description": "Search the support ticket database by free text. Returns at most 20 tickets, newest first, each with id, title, status and a 200-character snippet.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query":  { "type": "string", "description": "Free-text search terms" },
          "status": { "type": "string", "enum": ["open", "pending", "closed"] },
          "limit":  { "type": "integer", "minimum": 1, "maximum": 20, "default": 10 }
        },
        "required": ["query"]
      }
    }
  ]
}

Those three fields — name, description, schema — are the same three things you were already writing by hand into your provider's tool-calling API. The change is where they come from. Previously each consumer's codebase held its own copy, typed in by whoever did that integration, drifting out of date the moment the limit cap moved from 20 to 50. Now there is one copy, it lives next to the implementation that enforces the cap, and every client reads it fresh on connection.

Reading it fresh is what actually removes the adapter, and the chain is short enough to check link by link. A client needs three things before it can call anything: a name to send, a description good enough to decide with, and an argument shape to fill in. Those three are the only things it needs, because everything else about the call — where the process is, what language it is written in, how it reaches the database — is on the far side of a boundary the client never crosses. All three arrive in one response to one request the client already knows how to send. So the client can be finished, compiled and shipped before your tool exists, and it will still be able to call your tool, because the information it was missing is information it fetches rather than information it was built with. So the server's half of "no adapter" is not that the work got smaller, but that it moved to a step which happens at runtime and therefore happens for consumers nobody has met.

The other half of "no adapter" is what the consuming team actually does instead, and it is small enough to show. A compliant client is configured with a list of servers it may connect to, and the entry is a description of how to reach one — a command to run for a local process, or a URL for a remote one — with no reference to any particular tool:

json
{
  "mcpServers": {
    "support-tickets": {
      "url": "https://tools.internal.example.com/tickets/mcp",
      "headers": { "Authorization": "Bearer ${SUPPORT_TOOLS_TOKEN}" }
    }
  }
}

Nothing in that entry names search_tickets, which is the property that matters. The consuming team does not know what the server offers when they write the config, and they do not need to redeploy when the answer changes, because the answer arrives at connection time. Add create_ticket next quarter and every connected client picks it up on its next connect, with zero work on their side. This is also the point at which the arrangement starts to feel unfamiliar to a backend engineer, because the interface is not pinned at build time by a generated stub or a shared type package. It is read at runtime from the server, in the shape above, which is the same trade a database driver makes when it reflects a schema instead of compiling against one.

Wired per clientBehind one server
Adding the fifth consumeryou or they write an adapterthey add a config entry
Who writes the integrationthe consuming team, per SDKnobody; it already exists
Where the schema livesone copy per consumer codebaseone copy, next to the handler
A schema change ships whenevery consumer redeploysthe server redeploys
Consumer learns what existsfrom your docs, by readingfrom the server, at connect time
Runtime cost per calla function calla serialized round trip

The last row is the honest entry and the topic returns to it twice, because a protocol boundary is not free and the arithmetic above only pays when the consumer count is genuinely greater than one.

It is worth reading that last row as a mechanism rather than as a price tag, since the mechanism is what makes the price unavoidable. A function call hands a pointer to a value that already exists in memory, and the callee reads the same bytes the caller wrote. Two processes have no shared memory to point into, so the argument has to be turned into a sequence of bytes that means the same thing on both sides, written somewhere the other process can read, parsed back into a value there, and then the same three operations run again in reverse for the result. None of those steps has an equivalent in an import, and none of them can be optimised away while the two ends remain separate processes. The cost is therefore structural: it is what "separate process" means, and the rows above it are what that cost buys.

The three things it is constantly confused with

The first confusion is that MCP replaces tool calling. It does not, and the correction is important enough to have its own chapter later in this topic (chapter 3, on which layer actually speaks the protocol). The provider's tool-calling API is still what the model uses to request an action; MCP is how your application finds out which actions are available and where to send them. Both are present in a working system, doing different jobs at different layers.

The second confusion is that MCP is a general RPC framework, in which case you would reasonably ask why anyone needs one more. Its scope is narrower: it standardises the surface an application exposes to a model — tools it can call, resources it can read, prompt templates it can fill — and deliberately says nothing about how two autonomous agents negotiate work between themselves. That second problem has its own protocols, and the survey A survey of agent interoperability protocols: MCP, ACP, A2A, ANP is the fastest way to see the boundaries drawn; the short version is that MCP sits between an application and a capability, while A2A sits between one agent and another. Reaching for MCP to make two services talk is using a model-context protocol as a message bus, which will work and will be strange.

The third is that MCP is a Claude feature, which follows from Anthropic having published it and shipped the first clients. The specification is open and the client list is not vendor-bound — editors, desktop applications and agent frameworks from unrelated vendors implement it, which is the only reason the arithmetic above works at all. A protocol that one vendor's client speaks converts an M×N problem into an M×N problem with extra steps. The value is entirely in the breadth of the client population, so when you evaluate whether to build a server, the question worth asking is how many compliant clients your consumers are actually running, and not whether the spec is nominally open.

That last sentence is the one place in this chapter where a comfortable belief is wrong, and it is worth turning back into arithmetic rather than leaving as a warning. The saving came from replacing a per-pair cost with a per-side cost: each capability writes one server, each consumer's application writes one client, and the total is the two counts added rather than multiplied. The substitution only applies to a consumer whose application already contains a client. A consumer running something that speaks nothing gets no relief at all from the specification being public — somebody still writes that one pair by hand, and now they write it against a protocol as well as against your service, which is strictly more work than the adapter they would have written anyway. Openness is what made a broad client population possible; the population is what makes the addition true. Confusing the two is how a team ships a server for consumers who then ask them for an HTTP endpoint.

There are also things it plainly does not give you. It does not authenticate your users, it does not authorise them per tool, it does not shard, rate-limit or bill, and it does not make a badly described tool discoverable in any useful sense — a client can read your description, and if the description is vague the model will misuse the tool exactly as it did before. Nothing about the protocol improves the quality of what you exposed. It moves where the definition lives and who is allowed to reach it, and everything downstream of that — whether the tool is any good, whether the caller was entitled to run it, whether the result is worth the tokens it occupies — is exactly as much your problem as it was.

The description is the one of those worth following through, because "discoverable" sounds like something a protocol could plausibly provide and is not. Discovery in the protocol sense means a client can obtain the three fields; it makes no statement about what is in them. The description field is free text, nothing validates it, and the party that reads it in order to decide whether to call the tool is a model rather than a person who could ask you what you meant. So a description that omits the twenty-row cap produces a caller that asks for five hundred rows, and a description that omits the ordering produces a caller that sorts the results again itself — in both cases from a server that is fully compliant and fully discoverable. Standardising the envelope does not standardise what anyone put inside it, and the misunderstanding here is a common one because the two words sit so close together.

The claim that the same server runs unmodified under four clients is also, at this point, only a claim. It is testable, it fails in a specific and recognisable way, and it gets turned into an experiment with a real failure condition later in this topic (chapter 6, running one server under two clients that disagree). What can be settled now is the shape of the bargain rather than its price.

Reuse attaches to the server. It does not attach to the function, which is why a function called by one agent gains nothing at all by being wrapped: there is no adapter being avoided, because you were never going to write one for yourself. The honest form of the gain is the narrow one, and it is worth being able to say it in a design review without reaching for the word standardisation. Any compliant client — including ones you will never hear about — can connect, discover the tool and call it, and the schema they read is the one sitting beside the handler that enforces it.

That property is what the rest of this topic either delivers or bills you for. The machinery that delivers it is a handshake, a discovery step and a transport, each of which can fail in its own way before your code is reached. The bill arrives in milliseconds per call, in a deployable that now has a version and an on-call story, and in a class of failure that a Python import simply does not have. Both halves are measurable, and a decision about whether to build a server is a decision about the count of consumers that will amortise them.

What MCP is and why it exists — read the current version at https://daviddominguez.dev/learn/en/what-mcp-is-and-why-it-exists/mcp-server

Back to top