MCP vs API: When You Need Which, With Real Benchmarks
I built the same service as a REST API and as a stateless MCP server, then measured latency, throughput, bytes and the tokens a model reads. Here is when you need MCP, when a plain API wins, and how to wrap one with the other.

On this page
MCP vs API is not really a choice between two rival technologies. An API (usually REST) is how software talks to your service: your code calls known endpoints. MCP (Model Context Protocol) is how an AI application discovers and calls tools at runtime, and almost every MCP server sits on top of an API. The real question is who is calling: your code, or a model.
I built the same five-operation projects service both ways on one machine, as a REST API and as a stateless MCP server with the official TypeScript SDK, and measured what each costs. I measured latency, throughput, bytes on the wire and, most important for agents, the tokens a model has to read. I also measured the MCP server that runs this site. Every number below comes from those runs.
TL;DR: MCP or API, which should I use?#
Use a REST API when your own code is the caller: web and mobile apps, cron jobs, webhooks, service-to-service calls and fixed agent pipelines you control. Add an MCP server when AI clients you don't control (Claude, ChatGPT, Cursor, VS Code, Copilot) should use your service, or when the model must pick tools at runtime. Most products need both, with the MCP server wrapping the API.
What is the difference between MCP and an API?#
An API is a contract for programs: fixed endpoints, documented ahead of time, called by code a developer wrote. MCP is a contract for AI applications: a server lists its tools with names, descriptions and JSON Schemas, and a model decides at runtime which one to call. MCP doesn't replace your API. It describes a curated slice of it in a form a model can use.
MCP was open-sourced by Anthropic in November 2024. In December 2025 Anthropic donated it to the Agentic AI Foundation under the Linux Foundation, alongside OpenAI's AGENTS.md. The announcement put the count at "more than 10,000 published MCP servers" and listed Claude, Cursor, Microsoft Copilot, Gemini, VS Code and ChatGPT as adopters.
The MCP tools specification is explicit about who is in charge: "Tools in MCP are designed to be model-controlled, meaning that the language model can discover and invoke tools automatically." That one sentence is the whole difference. With an API, a developer decides what to call. With MCP, the model does.
Is an MCP server just an API wrapper?#
Usually, yes, and that's fine. In practice an MCP server is a thin adapter: each tool validates its arguments and then calls your existing service or REST API. What MCP adds is a standard way to list tools, a standard call format, tool annotations (read-only, destructive, idempotent), and an OAuth 2.1 flow every MCP client already understands. It does not add business logic.
Here is the same operation exposed both ways, sharing one Zod schema:
// store.ts: one schema, used by REST validation, OpenAPI and the MCP inputSchema
export const SlugInput = z.object({
slug: z.string().describe('Project slug, e.g. project-42'),
})// REST: GET /api/projects/:slug
if (req.method === 'GET' && slug) {
SlugInput.parse({ slug })
const p = ops.get(slug)
return p ? json(res, 200, { project: p }) : json(res, 404, { error: 'not_found' })
}// MCP: the same operation as a tool (@modelcontextprotocol/server 2.3.1)
server.registerTool(
'get_project',
{
description: 'Get one project by slug.',
inputSchema: SlugInput,
annotations: { readOnlyHint: true },
},
async ({ slug }) => {
const p = ops.get(slug)
return p
? { content: [{ type: 'text', text: JSON.stringify({ project: p }) }], structuredContent: { project: p } }
: { content: [{ type: 'text', text: `No project with slug ${slug}` }], isError: true }
},
)Notice the error path. REST returns a 404 your code branches on. MCP returns isError: true with a sentence, because the reader is a model that will decide what to try next.
How does MCP work compared to REST?#
REST uses many URLs and HTTP verbs. MCP uses one endpoint and JSON-RPC: every call is a POST /mcp with a method such as tools/list or tools/call. Under the 2026-07-28 spec MCP is stateless, so each request carries its protocol version and client capabilities in _meta, plus Mcp-Method and Mcp-Name headers.
The same lookup, both ways:
# REST
curl http://127.0.0.1:3201/api/projects/project-42
# MCP (2026-07-28, stateless, JSON response mode)
curl http://127.0.0.1:3202/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' -H 'Mcp-Name: get_project' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"get_project","arguments":{"slug":"project-42"},
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"8"},
"io.modelcontextprotocol/clientCapabilities":{}}}}'The REST response was 334 bytes on the wire. The MCP response was 758 bytes, because the result carries the data twice: once as a JSON string in a text block and once as structuredContent. That's by design. The spec says a tool that returns structured content "SHOULD also return the serialized JSON in a TextContent block" for backwards compatibility.

How I benchmarked MCP vs REST#
- Service. 1,000 in-memory projects with five operations: list (search and tag filter), get, create, update and delete. Both servers share the same store and the same Zod 4.6.7 schemas, so only the protocol layer differs.
- REST. Plain
node:httpwith Zod validation, port 3201. - MCP, direct.
@modelcontextprotocol/server2.3.1 withcreateMcpHandlerin JSON response mode, a freshMcpServerper request (the SDK's documented stateless pattern), and tools calling the store in-process. - MCP, wrapping REST. The same MCP server, but each tool calls the REST API over HTTP with
fetch. This is the common real-world setup. - Runs. Node.js 24.22.0 on an 8 vCPU Intel Xeon VM with 15 GB RAM, everything on localhost. Latency: 20,000 sequential requests per case after 2,000 warm-up requests, timed with
process.hrtime. Throughput: autocannon 8.0.0 for 10 seconds after a 3-second warm-up, with 1 and 50 connections. All responses were 200 with no errors. - Tokens. Counted with OpenAI's
o200k_basetokenizer (tiktoken), on compact JSON. Other models tokenize differently, so treat these as relative sizes.
Localhost removes network time on purpose. On a real network both protocols pay the same round trip, so the gap below is pure protocol and framework overhead.
Is MCP slower than a REST API?#
Yes, per request, by a lot in relative terms and very little in absolute terms. A get_project call took 424 µs at the median over MCP against 76 µs over REST, about 5.6 times slower. At 50 connections one Node process served 66,486 REST requests per second against 4,595 MCP requests per second, about 14 times fewer. When the MCP tool called the REST API instead of the store, the median doubled again to 848 µs.
Where does the time go? A CPU profile of the MCP server under load showed 16.7% of samples in the SDK's request pipeline, 11.5% in Zod (parsing plus JSON Schema conversion), and most of the 25% in Node internals spent converting Node requests into Web Request objects and back. Building the per-request McpServer itself was cheap: 13.4 µs for five tools.
Does that matter? For agents, no. A model turn takes hundreds of milliseconds to seconds, so 0.35 ms of protocol overhead is noise. For a high-traffic API that your own frontend calls thousands of times a second, it matters a lot. That's one more reason not to route your app's own traffic through MCP.
How many tokens do MCP tools cost?#
The real cost of MCP is context, not CPU. Clients usually send every tool definition, with name, description and JSON Schema, to the model as input tokens on each request. My five tools cost 591 tokens as returned by tools/list, and 488 tokens once reshaped into plain function-calling definitions. The MCP server behind this site exposes 21 tools that add up to 5,949 tokens.

Three things stand out. First, an OpenAPI document is a poor thing to paste into a prompt: it describes responses, status codes and servers the model doesn't need, and for the same operations it was 1.6 times the raw tool list and 1.9 times the function definitions. Second, one rich tool can cost more than ten small ones. update_post alone is 17% of my site's tool budget because its schema lists every editable field. Third, results are cheap if the client is sensible: the text block and the structuredContent were 46 tokens each, the same as the REST body.
Large catalogs get expensive fast. Anthropic's engineering team described one setup where loading tool definitions up front took 150,000 tokens. Letting the agent load only the definitions it needed through code execution with MCP cut that to 2,000 tokens, "a time and cost saving of 98.7%." Clients are adapting too. In Claude Code, tool search is on by default, so "only tool names and server instructions load at session start."
When should I use MCP instead of an API?#
Use MCP when the caller is an AI client you don't build, or when the model must choose actions at runtime. Use a plain API, called from your own code, when the workflow is known in advance. A rule of thumb: if you could write the sequence of calls as a function today, write the function. If a person would phrase the task differently every time, give a model MCP tools.
That last row is why "MCP vs API vs Skills" posts keep trending. They answer different questions. The API is the capability, MCP is how a model reaches it, and a skill is the instruction manual for using it well.
How do I add an MCP server to an existing REST API?#
Wrap the API, don't rewrite it. Keep your REST endpoints as the source of truth, then expose a curated set of tools that call them. Reuse your validation schemas, return errors as sentences, and mark read-only and destructive tools with annotations. Start with read-only tools, then add writes behind a narrower OAuth scope.
// MCP tool that wraps an existing REST endpoint
const API = process.env.API_URL // e.g. https://api.example.com/api/projects
server.registerTool(
'update_project',
{
description: 'Change fields on a project. Only the fields in patch are updated.',
inputSchema: UpdateInput,
annotations: { idempotentHint: true },
},
async ({ slug, patch }, ctx) => {
const r = await fetch(`${API}/${slug}`, {
method: 'PATCH',
headers: {
'content-type': 'application/json',
// forward the caller's own token so REST authorization still applies
authorization: `Bearer ${ctx.http?.authInfo?.token}`,
},
body: JSON.stringify(patch),
})
if (r.status === 404) return { content: [{ type: 'text', text: `No project with slug ${slug}` }], isError: true }
const data = await r.json()
return { content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data }
},
)Design rules I follow, learned from running an MCP server for this site's content:
- Expose tasks, not endpoints. One
publish_posttool beats making the model chain three PATCH calls. - Keep descriptions short and front-loaded. Claude Code reads only part of a long description when it matches tool search, so put the key words first.
- Use scopes. This site issues per-app tokens with scopes like
posts:readandposts:write, so a client with a read-only token can't call write tools. - Treat annotations as hints. The spec says clients "MUST consider tool annotations to be untrusted unless they come from trusted servers." Enforce permissions on the server.
- Keep a human in the loop for destructive tools. The same spec says there "SHOULD always be a human in the loop with the ability to deny tool invocations."
For a full build, see my guide to building an MCP server in Next.js with route handlers and bearer auth.
Does MCP replace REST APIs?#
No. MCP depends on APIs. Your frontend, mobile apps, partners and background jobs still need fast, cacheable, versioned HTTP endpoints, and MCP gives them nothing. What MCP replaces is the per-client plugin: instead of writing a ChatGPT plugin, a Cursor extension and a Claude integration, you write one MCP server and every compliant client can use it.
Common mistakes when choosing between MCP and an API#
- Generating one MCP tool per endpoint. A 60-endpoint OpenAPI file becomes 60 tools and thousands of tokens on every request. Curate.
- Routing your own app's traffic through MCP. In my test that cut single-process throughput about 14-fold for nothing.
- Pasting OpenAPI specs into prompts. The same operations cost 1.6 to 1.9 times more tokens as compact OpenAPI than as tool definitions.
- Forgetting the double payload. Text plus
structuredContentroughly doubles response bytes. Keep results small and paginated. - Trusting annotations for security.
readOnlyHintis a hint. Check scopes on the server for every call. - Building on session state. Since the 2026-07-28 spec there are no sessions. Pass explicit handles in tool arguments instead; see stateless MCP servers.
Key takeaways#
- An API is for code that knows what to call; MCP is for AI models that discover tools at runtime and decide what to call.
- Most MCP servers should wrap an existing API, reusing its validation schemas and authorization.
- In my Node.js 24 test, an MCP tool call took 424 µs at the median against 76 µs for the same REST call, and 848 µs when it wrapped the REST API.
- One Node process served 66,486 REST requests per second against 4,595 over MCP, so keep your app's own traffic on REST.
- Tokens are MCP's real cost: 5 tools took 488-591 tokens, and this site's 21-tool MCP server takes 5,949 tokens.
- The same 5 operations cost 933 tokens as compact OpenAPI, 1.6 to 1.9 times the tool definitions, so don't paste API specs into prompts.
- Expose a small set of task-level tools, scope them per client, and use agent skills to teach the workflow.
Questions
Share


