Somewhere in the last year, "do we need an MCP server?" joined the list of questions that arrive in a brief before anyone has said what the thing is for. It is usually framed as MCP vs API, as though the Model Context Protocol were a newer kind of API, something to migrate to. It is not. Open almost any MCP server and there is an ordinary API call at the bottom of it.
This is the developer-side version of a guide we wrote for the people who commission these projects. It skips the business case and looks at what goes over the wire, where the real work in a server sits, and the part of the design that the demos leave out.
What actually goes over the wire
MCP is JSON-RPC 2.0, carried over standard input and output when the server runs on the same machine as the host, or over Streamable HTTP when it runs somewhere else. The host is the application a person uses, such as Claude, ChatGPT, VS Code or Cursor. Inside it, a client holds a connection to one server, and the server is the piece you would build.
Discovery is the first difference from a normal API. Nobody reads documentation. The client asks the server what it can do, and the server answers with names, descriptions and JSON Schemas:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"tools": [
{
"name": "search_orders",
"description": "Find orders by customer email or order number. Returns at most 10 orders, newest first.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A customer email address or an order number such as 10482"
}
},
"required": ["query"]
}
}
]
}
}
The model reads that list and decides for itself which tool fits what the user just asked. When it picks one, the client sends a call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_orders",
"arguments": { "query": "*Emails are not allowed*" }
}
}
The specification examples omit one detail, and so do these: every request also carries a _meta block with the protocol version, client information and client capabilities. That is new. The revision dated 28 July 2026 removed the opening handshake and protocol-level sessions, so each request now describes itself, much like an ordinary stateless API call. A lot of what was written about MCP in 2025, including most of the tutorials, describes the older, session-based version.
The bottom of every server is an API call
Follow that tools/call through a typical server and the chain is short. The server validates the arguments, turns them into an HTTPS request to the API the system already had, and wraps the response as a tool result. GitHub's official MCP server calls GitHub's API. The Blender and Hugging Face servers we use every day are thin layers over Blender's Python API and the Hugging Face Hub API.
That has a practical consequence for estimates. If a system has no API, it does not need an MCP server yet. It needs an API, and the server comes after it. If the API exists and is decent, the protocol side of the server is small. The official SDKs cover TypeScript, Python, C#, Go, Java and Kotlin among others, and a server that wraps three endpoints runs to a few hundred lines. A demo really is a day's work. Everything below is why production is not.
An endpoint is written for a programmer who reads it once, writes code against it and then calls it the same way forever. A tool is read by a model on every conversation, and the model decides on the spot whether this is the right one. A vaguely named endpoint costs a developer ten minutes. A vaguely described tool costs you every conversation in which the model picks the wrong one, and nobody notices, because the answer still sounds confident.
The common mistake is to mirror the API one endpoint per tool. Anthropic's own guidance on writing tools names this directly as an error: tools that "merely wrap existing software functionality or API endpoints". Its example is contacts. The mirrored version looks like this:
{
"name": "list_contacts",
"description": "List contacts.",
"inputSchema": {
"type": "object",
"properties": {
"page": { "type": "integer" },
"per_page": { "type": "integer" }
}
}
}
To find one person, the model has to page through everyone, reading every page into its context to check it. The version written for the model does the search on the server:
{
"name": "search_contacts",
"description": "Find contacts by name, email or company. Use this before creating a contact, to avoid duplicates.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Part of a name, an email address or a company name"
}
},
"required": ["query"],
"additionalProperties": false
}
}
Fewer, higher-level tools, with descriptions that say when to use them as well as what they do. The schema descriptions matter as much as the tool description: "an order number such as 10482" saves a round trip in which the model guesses the format wrong.
There is also a cost that lands on the model's side rather than the server's. Every tool definition is loaded into context, on every conversation. Anthropic's engineers worked through an example in which tool definitions and intermediate results passing through the model came to 150,000 tokens, and cut it to about 2,000 by letting the model write code against the servers instead. That was one worked example, not a benchmark, but the direction holds. Every tool you expose is a cost on every conversation, so a few good tools beat a mirror of your whole API.
Errors are written for the model, too
MCP separates two kinds of failure. A protocol error, such as an unknown tool or a malformed request, comes back as a standard JSON-RPC error, and the model can rarely do anything with it. A tool execution error comes back as a normal result with isError: true, and the specification says clients should pass it to the model so it can correct itself:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "No order 10428 found. Order numbers are 5 digits; did you mean 10482? Search by customer email if the number is uncertain."
}
],
"isError": true
}
}
Compare that with the 404 Not Found the underlying API returned. The API's error is fine for code that checks a status. The model needs a sentence that tells it what to try next. Writing those sentences is a surprising share of the work, and it is the share that makes the difference between a server that recovers and one that gives up and apologises to the user.
State, now that there are no sessions
With sessions gone from the protocol, a server cannot keep implicit per-connection state. The specification's guidance for anything that has to span calls, such as a basket, an open browser context or a transaction, is an explicit handle: a creation tool returns an ID, and later calls take it as an argument. The model carries it forward.
Two parts of that guidance deserve to be read twice. For an authenticated server, a handle is a name, not a capability: check the caller's authorisation against it on every call, because nothing stops a model, or someone steering it, from passing a handle that belongs to somebody else. And state the handle's lifetime in the creation tool's description, so the model knows a basket expires before it plans around one.
The part the demos skip
An MCP server gives a model hands, and a model takes instructions from anything it reads. A year of incidents shows what that means in practice. In April 2025 Invariant Labs demonstrated tool poisoning, instructions hidden in a tool's description that the user never sees and the model obeys. In May 2025 the same team showed a crafted issue in a public GitHub repository making an agent with the official GitHub server leak data from private ones, and were clear that the fault was the architecture, not the server's code. In June 2025 Asana took its MCP server offline for almost two weeks after a logic flaw exposed some data to other organisations. In July 2025 a flaw in mcp-remote, a widely used connector, allowed remote code execution on the machine of anyone who connected it to an untrusted server. In September 2025 a malicious npm package posing as an email server blind-copied every message it sent to its author.
Simon Willison named the pattern behind most of these the lethal trifecta: access to private data, exposure to untrusted content and a way to send data out. Any two can be managed. With all three, anyone who can put text in front of the model, in a support ticket, a product review or a web page, can try to make it send your data somewhere.
The specification helps where it can. It requires servers to validate inputs, enforce access control, rate limit tool calls and sanitise outputs. For remote servers it recommends OAuth 2.1, requires that every token was issued for your server, and forbids passing the client's token through to the API behind it. It also tells clients to treat tool annotations, the hints a server gives about how a tool behaves, as untrusted unless the server is. None of that can make a server safe if the server is allowed to do too much. Give an MCP server the narrowest permissions that do the job, because the most hostile text it ever reads gets the same permissions you did.
In code, that comes down to a handful of decisions. Read-only tools by default, and write tools behind an explicit confirmation. One server per trust boundary, rather than one that can do everything. Dependencies pinned and reviewed, rather than whatever the registry serves today. Every call logged with who asked and what happened. And never a tool that runs arbitrary SQL or shell commands against production: a tool that accepts free-form SQL hands every rule you have to whoever manages to talk the model into using it.
When not to build one
The protocol earns its place when whatever calls your system is a model somebody else chose: customers who work inside AI assistants and want your product there, or staff who want to ask internal systems questions from the assistant they already use. It does not earn it for a fixed process with known steps. "When an order arrives, create the invoice and email it" wants deterministic automation calling the API directly; a model deciding each step adds cost and a new way to fail, and gains nothing. A single integration between two of your own systems wants a plain API call.
If a server is the right answer, build the smallest one that answers the most common question, read-only, put it in front of real users and read the logs. The tool calls show what the model actually needs faster than any design document.
The original
This is the developer-side version of a guide on our site, written for the people deciding whether a project needs an MCP server at all. The original has the full side-by-side table of MCP and a REST or GraphQL API, the MCP vs function calling question, and answers to what clients ask about it: whether MCP only works with Claude, whether a website needs one, and how long a production server takes.
MCP vs API: what an MCP server actually adds
This copy is a snapshot from the day it was posted, and it will not be updated here. The original on our site is the version we keep current: when a study publishes new figures, a price changes or a tool is renamed, that is where the correction lands. If you want the up-to-date version, follow the link above.
Also available in German and Romanian.
Sources
The wire format, the _meta requirement, the two kinds of error, the handle pattern for state and the server and client security requirements are from the Model Context Protocol Tools specification, revision 2026-07-28, and the removal of the handshake and sessions is in that revision's changelog. The OAuth requirements and the ban on token passthrough are from the Authorization chapter. The advice against mirroring endpoints and the contacts example are from Anthropic's Writing effective tools for agents, and the token figures from Code execution with MCP. The incidents are from Invariant Labs on tool poisoning and the GitHub exploit, BleepingComputer on Asana, JFrog on mcp-remote and The Hacker News on the malicious npm package. The lethal trifecta is Simon Willison's. The order-search examples are ours.
Which went wrong first on the servers you have shipped: the model picking the wrong tool, or the right tool being allowed to do too much?