MCP vs API
Before anything else, here’s the short version of the difference:
Direct API is best when you’re a developer writing one specific app that talks to one specific service you already know well. You know the exact endpoint, the exact auth, the exact response shape, ahead of time, and you hardcode it. Fewer moving parts, lower latency, nothing to explain. This is what almost all traditional software does.
MCP is best when the caller is an AI agent (like Claude) that needs to use tools it wasn’t specifically coded for, decided at runtime, without a developer writing custom integration code for every single service in advance. That’s the actual reason MCP exists, not because it’s technically faster or simpler, but because it solves a different problem: standardizing how an LLM discovers and calls a tool, the same way regardless of whether the tool is Slack, GitHub, or your company’s internal database.
That’s the short answer. The rest of this post is me proving it to myself by actually building both paths and watching them run.
I put together a small dashboard that fetches the exact same data from two real services, GitHub and Slack, using two completely different routes. One side calls the API directly. The other side goes through a real MCP client that spawns a real MCP server, and it’s the server that actually calls the API. Same data, two different ways of getting it, running side by side so I could watch both happen in real time instead of just reading about the difference.
Here’s what it showed, and why that extra layer on the MCP side exists at all.
What the two paths look like
Structurally, here’s the difference between the two routes:
The direct path is one hop: send a request, get a response back. The MCP path is three hops: the client talks to the server, the server talks to the real API, and the server hands the result back to the client. Just from the shape of this diagram you can already predict the outcome. When I timed both sides on identical requests, MCP was slower every single time. That’s not a flaw in how I built it, it’s just what having a middleman costs.
So the obvious next question is: if it’s slower, why does it exist?
What’s actually happening during those extra hops
“MCP handshake” is a phrase that sounds abstract until you watch it happen step by step. Here’s the real sequence, captured from an actual run of the dashboard:
Three real exchanges happen before any actual data comes back. First, initialize, where the client and server introduce themselves, agree on a protocol version, and the server states what it's capable of. Second, tools/list, where the client asks "what can you actually do?" and gets back a list of tool names, without those names ever being hardcoded on the client's side. Third, tools/call, where the client finally invokes a specific tool with arguments, and only now does the server go make the real HTTP call.
Get nithesh kumar’s stories in your inbox
Join Medium for free to get updates from this writer.
The direct path skips all of that. Build a URL, attach an auth header, send the request, parse the response. One step, no negotiation, no discovery.
The handshake, step by step
It’s worth slowing down on what “handshake” actually means here, because the four steps are simpler than the word suggests.
- The client sends
initializeover stdio, the same stdio you'd normally associate with a plain command-line program's input and output, except here it's carrying JSON-RPC messages between two processes instead of text a human would type. There's no network socket anywhere in this step, the client spawns the server as a subprocess and talks to it through its stdin and stdout pipes directly. - The server responds on that same stdio channel with its protocol version and its declared capabilities. This is the part of the exchange where the two sides agree they’re speaking the same version of the protocol before anything else happens.
- The client then sends
tools/list. This is the client asking, in effect, "given that we've agreed on a protocol, what can you actually do for me?" Nothing about the answer is hardcoded on the client's side, whatever the server currently exposes is what comes back. - The server answers with the real catalog, tool names and descriptions, as they exist on the server right now. Add a new tool to the server tomorrow, and the very next
tools/listcall reflects it automatically, with zero changes to the client.
So why would anyone choose the slower path
Because the two paths aren’t built for the same kind of caller.
When I write an ordinary app, I’m the one deciding what gets called. I read Slack’s docs once, I learn the endpoint is conversations.list, I know it needs a channels:read scope, and I bake all of that into my code permanently. From that point on, my app isn't discovering anything at runtime, it's just executing a decision I already made in advance.
An AI agent can’t do that. It has no way to have every possible API memorized and hardcoded ahead of time, and new tools show up constantly. What it needs instead is a way to ask, right at the moment it’s running, "What tools are available to me?” and then decide which one fits the task and what arguments to send. That’s exactly what the tools/list step in the sequence diagram is doing. It isn't overhead for its own sake, it's the mechanism that lets one agent plug into Slack today and something totally unrelated tomorrow, speaking the same protocol both times, without a developer rewriting the agent for every new service that shows up.
Compare the two halves of that diagram. Without MCP, every new tool means new custom code baked directly into the agent itself. With MCP, the agent’s side never has to change, it always speaks the same three-step protocol, and each new service just needs a thin server written for it once, by whoever maintains that integration. Mine mcp_server.py didn't invent any new functionality at all. It just wrapped GitHub's and Slack's existing REST APIs in MCP's shape so an MCP client could discover and call them without me writing agent-specific code for either service.
How a tool gets its name, and how it gets picked
Two questions sit underneath everything above, and they’re worth answering directly instead of leaving implied.
How does a tool get its name and description in the first place? Nobody fills out a separate form somewhere. A server writer writes an ordinary function, gives it a sensible name, and puts a docstring on top explaining what it does. That’s the entire registration process. The function’s name becomes the tool’s name, and the docstring becomes the tool's description, both read automatically the moment the server starts, nothing is typed twice, nothing is kept in sync by hand. Write the function well, name it clearly, describe it honestly, and the catalog is correct by construction. Write a vague docstring, and the catalog will be technically present but useless, because the description is the only information anything downstream has to go on.
What this means in practice
For a single, fixed integration where you already know exactly what you’re calling, MCP buys you nothing but extra latency. Direct API wins outright, no contest, and my dashboard’s timings back that up plainly.
But the moment you want one client that can talk to any number of tools it doesn’t know about in advance, deciding at runtime which ones to use, that extra hop stops being overhead and becomes the entire reason the protocol exists. It’s the price of not having to hardcode the world.
Reading about MCP told me what the protocol was called. Building this and watching the handshake happen, watching tool names get discovered instead of hardcoded, and watching the timings confirm the tradeoff was real are what actually made the answer stick.








