I built the same small service as a REST API and as a stateless MCP server and measured latency, throughput, bytes and the tokens a model has to read. The original, with any updates, lives on fabwaseem.com.
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.
| REST API | MCP server | |
|---|---|---|
| Caller | Your code, written ahead of time | An AI model inside an MCP client |
| Discovery | Docs or an OpenAPI file a developer reads |
tools/list at runtime |
| Wire format | HTTP verbs, paths, status codes | JSON-RPC 2.0 over Streamable HTTP or stdio |
| Auth | Whatever you choose (keys, OAuth, sessions) | OAuth 2.1 profile defined by the spec, or bearer tokens |
| Latency in my test (p50) | 76 µs | 424 µs (848 µs when it wraps the REST API) |
| Throughput, 1 Node process | 66,486 req/s | 4,595 req/s |
| What it costs a model | Nothing until you write function schemas | Every tool definition, as input tokens |
| Testing | Deterministic unit and contract tests | Protocol tests plus prompt-dependent evals |
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.
Case (get_project) |
p50 | p90 | p99 | Mean |
|---|---|---|---|---|
REST GET /api/projects/:slug
|
76 µs | 122 µs | 274 µs | 95 µs |
MCP tools/call (direct) |
424 µs | 709 µs | 1,164 µs | 500 µs |
MCP tools/call (wraps REST) |
848 µs | 1,216 µs | 1,878 µs | 936 µs |
| Throughput, one Node process | 1 connection | 50 connections | Response size |
|---|---|---|---|
| REST get | 30,944 req/s | 66,486 req/s | 334 B |
| MCP get (direct) | 3,216 req/s | 4,595 req/s | 758 B |
| MCP get (wraps REST) | 1,622 req/s | 3,116 req/s | 758 B |
| REST list (10 results) | 27,566 req/s | 48,022 req/s | 1,707 B |
| MCP list (10 results) | 2,733 req/s | 3,285 req/s | 3,700 B |
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.
| What the model reads | Tokens (o200k_base) |
|---|---|
5 tools, raw tools/list result |
591 |
| Same 5 tools as function-calling definitions | 488 |
| OpenAPI 3.1 document for the same 5 operations (compact) | 933 |
| Same OpenAPI document, pretty-printed | 1,717 |
| fabwaseem.com MCP server, 21 tools | 5,949 |
Its largest tool, update_post
|
1,027 |
Its smallest tool, get_site_info
|
89 |
One get_project result: REST body |
46 |
One get_project result: full MCP result envelope |
140 |
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.
| Scenario | Use | Why |
|---|---|---|
| Your web or mobile app's frontend | REST API (or server actions) | Fast, cacheable, no model involved |
| Stripe-style webhooks, cron jobs, ETL | REST API | Deterministic, testable |
| Let users manage your product from Claude, ChatGPT or Cursor | MCP server | Every MCP client can connect with no custom plugin |
| Your own agent with a fixed set of 3-5 actions | Function calling against your API | Same tokens as MCP, one less hop |
| Internal tools shared by several agents and IDEs | MCP server | Write once, every client discovers it |
| Teaching an agent how to use those tools | An agent skill on top of MCP or a CLI | Instructions load only when needed |
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.
Related: if you do expose MCP, lock it down: MCP security in 2026 covers the OAuth SDK CVEs and per-tool scopes. Choosing a framework for the REST side? I benchmarked Hono vs Express vs Fastify on Node 24, Node 26 and Bun.
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.
FAQ
What is the difference between MCP and an API?
An API is a contract for programs: fixed endpoints that code written by a developer calls. MCP, the Model Context Protocol, is a contract for AI applications: a server lists tools with names, descriptions and JSON Schemas, and a model decides at runtime which tool to call. Most MCP servers call an existing API underneath.
Is an MCP server just an API wrapper?
Usually, yes. A typical MCP server is a thin adapter that validates tool arguments and calls your existing service or REST API. MCP adds standard tool discovery through tools/list, a standard call format, tool annotations such as readOnlyHint, and an OAuth 2.1 flow MCP clients understand. It should not duplicate your business logic.
Is MCP slower than REST?
Per request, yes. In my test on Node.js 24.22 a tool call over MCP took 424 microseconds at the median against 76 microseconds for the same REST call, and one process served 4,595 MCP requests per second against 66,486 REST requests. For agents this is noise, because a model turn takes hundreds of milliseconds or more.
How many tokens do MCP tools use?
It depends on the schemas. In my test, five tools cost 591 tokens as returned by tools/list and 488 as plain function definitions, counted with the o200k_base tokenizer. The MCP server behind fabwaseem.com exposes 21 tools totalling 5,949 tokens, and its largest tool, update_post, costs 1,027 tokens by itself.
When should I use MCP instead of an API?
Use MCP when AI clients you don't build, such as Claude, ChatGPT, Cursor or VS Code, need to use your service, or when a model must choose actions at runtime. Use a plain API called from your own code when the sequence of calls is known in advance, for example in frontends, webhooks, cron jobs and data pipelines.
Does MCP replace REST APIs?
No. MCP depends on APIs. Frontends, mobile apps, partners and background jobs still need fast, cacheable, versioned HTTP endpoints. What MCP replaces is the per-client plugin: instead of building separate integrations for each AI app, you publish one MCP server that every compliant client can discover and call.
Should I convert my OpenAPI spec into MCP tools?
Use it as a starting point, not a one-to-one mapping. One tool per endpoint inflates the tool list the model reads on every request. In my test the same five operations cost 933 tokens as compact OpenAPI versus 488 to 591 as tool definitions. Curate a small set of task-level tools with short descriptions instead.
How do MCP, APIs and agent skills fit together?
They answer different questions. The API is the capability your service offers. MCP is how a model discovers and calls that capability from any compatible client. An agent skill, a SKILL.md file, teaches the agent the workflow and rules for using those tools, and it loads into context only when a task needs it.
Originally published on fabwaseem.com.


Top comments (2)
That last clause is the reason the sentence is not enough. REST hands the caller three answers that imply three different retry policies: 404 means do not retry, 409 means retry later, 422 means fix the payload before you retry. Flatten all three into isError plus prose and the model has to infer retryability from English, which is the one inference you do not want it making on a write path. "No project with slug project-42" and "idempotency key reused with a different payload" read identically to a retry loop.
The fix fits your existing shape, since you are already returning structuredContent on the success path. Put the machine-readable half there on the failure path too, something like { error: { code: "conflict", retryable: true, retry_after_ms: 500 } }, and leave the sentence in the text block for the transcript. A model parses structuredContent, and your double-payload pitfall barely applies on this path: an error object is a few fields rather than a duplicated result, nowhere near the 334 to 758 byte jump you measured on the success path.
Your pitfall list already says readOnlyHint is a hint and that scopes belong on the server. The idempotentHint on update_project is where that costs money: a host is free to ignore it and retry, so a non-idempotent tool still needs a server-side idempotency key if any retrying client can reach it. The annotation documents the intent, it does not make the second call safe.
Your run notes say all responses were 200 with no errors, so 848 µs for the wrapping case is the happy path by construction. If there is a second edition, the number that decides MCP cost for an agent is the one on the other side: how many extra tool calls an unparseable error buys you.
The 'who is calling' frame is the right way to settle this, and the benchmarks make the usually-handwavy MCP-vs-API debate concrete. One nuance worth adding from the integration side: for a domain the model barely knows — EDI is a good example — the MCP layer does double duty, because the tool descriptions carry know-how no amount of generic REST docs would give the agent. The API stays the source of truth for your own code; the MCP wrapper becomes the teaching layer.