MCP is Dum
MCP servers solve a problem that doesn’t exist.
The pitch sounds reasonable. Standardize how LLMs talk to tools. Build a server, expose some endpoints, plug it into Claude or whatever model you’re using, and now the model can call your service. Neat. Clean. Standardized.
It’s also redundant.
The Thing Claude Already Does
Hand Claude a service’s API docs, or just the URL to them, and ask it to write a CLI that talks to that service. You get a working binary. Auth, pagination, error handling, the whole deal. Takes a minute. Maybe two if the API is gnarly.
I’ve done this dozens of times. Stripe, GitHub, internal services, weird vendor APIs with PDF documentation. The model figures it out. The output is a binary you can run, inspect, version, and check into git. It does the thing.
So what is an MCP server doing that this doesn’t already cover?
The Training Data Problem
Here’s the part nobody wants to say out loud. The model knows the APIs. Deeply. It has seen the Stripe SDK in thousands of GitHub repos, in tutorials, in Stack Overflow answers, in every blog post ever written about charging credit cards. Same for GitHub’s API, AWS, Twilio, Slack, every major service that exists.
MCP doesn’t have that. MCP is new. The protocol spec, the server implementations, the various flavors of auth glue. None of it is in the training data at the depth that, say, requests.get() is. When you ask the model to use an MCP server, you’re asking it to operate in the thin part of its knowledge distribution. When you ask it to write a Python script that hits an HTTP API, you’re asking it to do the most well-documented thing in software history.
You can guess which one works better.
A Thousand Examples vs. One Wrapper
For every API under the sun, there are thousands of working code examples sitting in public repos. Idiomatic Python. Idiomatic Go. Idiomatic TypeScript. Different auth flows, different error handling patterns, different retry strategies. The model has absorbed all of it.
An MCP server is, at best, one wrapper around that same API. Usually a worse one. The MCP version picks one auth scheme, one set of operations, one opinionated subset of the actual surface area. You lose options. You lose the ability to combine calls in ways the wrapper author didn’t anticipate. You get a permuted, lossy projection of an API that the model already understands in full detail.
It’s strictly less powerful than just letting the model write the code.
What MCP Actually Is
Strip away the protocol marketing and an MCP server is a process that sits between the model and an API, exposing a handful of operations the model can invoke. At best it’s a thin wrapper around an existing API. At worst it’s the same wrapper but with an extra serialization layer, a new auth model, and a maintenance burden nobody asked for.
The MCP server is an injection point. That’s the whole job. You’re telling the model “here are some tools you can call.” The model calls them. The server forwards the call to the real API.
Why is there a server in the middle?
The Binary Beats the Server
When the model writes a binary, the binary is the integration. You can read it. You can run it from a shell. You can pipe its output. You can debug it with the same tools you debug every other piece of software. The model can read it back later and modify it. The integration is code, not configuration.
When the model talks to an MCP server, the integration is a long-running process you have to host, monitor, restart, and secure. You’ve turned a 200-line CLI into infrastructure.
What MCP Optimizes For
There’s exactly one case where MCP makes sense: the service has internal state or capabilities that aren’t exposed via any API at all. A local filesystem. A running IDE. A database connection pool you don’t want to recreate on every call. The MCP server holds state the binary couldn’t.
That’s a real niche. It’s also a small one. Most things people are building MCP servers for already have HTTP APIs. The MCP server is a wrapper. The wrapper is the product. The product is unnecessary.
A Worked Example: Gmail
I went looking for a Gmail MCP recently. Wanted Claude to search my inbox for something specific. Reasonable ask. Shouldn’t be hard.
It was hard.
Anthropic’s first-party connectors cover Asana, Atlassian, Box, Canva, HubSpot, Intercom, Linear, Notion, and monday.com. Not Gmail. So “the Gmail MCP isn’t working” turned out to mean “no Gmail MCP exists here,” which is a different kind of problem. Nothing to fix. Something to build.
Third-party Gmail MCP servers need you to authenticate against the Gmail API. That is not an API-key handoff. The dance is: spin up a Google Cloud project, enable the Gmail API on it, configure an OAuth consent screen, generate an OAuth Client ID of the correct type. Pick the wrong type and nothing works. The credentials.json I uploaded first was a service-account JSON, which looks identical to the OAuth client JSON but is a different identity entirely. A bot, not me. Service accounts cannot read personal mail unless you have Workspace super-admin domain-wide delegation plus code-level impersonation, which I do not.
The “right” alternative is a Desktop OAuth client. That one assumes the human consenting to the OAuth flow is sitting at the machine running the script when the browser pops. I wasn’t. Doing OAuth from a different device adds another layer: copy the consent URL out, copy the auth code back, handle redirect URIs, hope nothing in the chain rejects you.
Four nested problems instead of one. No built-in connector, type-sensitive credential format, browser-bound consent, remote operator.
We did it the proper way first. Throwaway venv with the Google API client libs. A full CLI at scripts/gmail_search.py wired for Desktop OAuth, token caching at ~/.config/claude-gmail/, ready to handle queries, paging, message bodies. Auth failed on the service-account JSON. The only paths forward were either re-issuing the correct client type or rewriting the script for service-account plus domain-wide delegation. Both fine. Neither simple.
You know what worked? I labeled the relevant messages in Gmail’s web UI, ran Google Takeout against that single label, and got an .mbox file. Python’s stdlib mailbox module iterates messages directly. Zero dependencies. The “import” half of the problem collapsed to a few lines. The auth problem disappeared entirely because Takeout is just me clicking buttons while logged in.
The MCP path: half a day of OAuth wrangling, multiple credential file types, and a long-lived process I’d have to babysit. The non-MCP path: click some labels, run Takeout, parse an mbox. Same end result. One of these is dumb.
A Second Worked Example: Slack
Same week. Different MCP. Same outcome.
I wanted Claude to read some Slack threads. There is a Slack MCP. It uses Slack’s official API, which expects a bot token. Sounds fine.
It is not fine if you are a workspace admin.
The official API path for a bot wants you to install a Slack app into the workspace, scope it to the channels it can read, and have a non-admin owner approve it. Admin self-approving your own app to read other people’s DMs and private channels is exactly the thing Slack’s permission model is designed to prevent, and rightly so. Every “easy mode” doc for the Slack MCP quietly assumes you are not the person responsible for security on your own workspace.
So you fall off the supported path and into the swamp. The “alternative” is the browser-token version: open Slack in the web client, fish your xoxc-* and xoxd-* tokens out of cookies and local storage, paste them into the MCP server’s config, and let it impersonate your browser session against Slack’s undocumented internal APIs. This is exactly the integration mode the MCP provider built the bot-token path to avoid. It is fragile. It breaks when Slack rotates session formats. It is, in any reasonable security policy, exfiltrating credentials you should not be exfiltrating.
I did it anyway, because the alternative was no integration at all.
Meanwhile, Slack’s web client speaks JSON over a perfectly readable internal API that the model already knows how to call. A 100-line Python script with the same browser tokens does the exact same job without a long-lived server holding them in memory. Same fragility. Less infrastructure. Fewer parties to trust.
Two MCPs in one week. Both fell apart on the auth boundary. Both worked instantly the moment I stopped pretending the MCP layer was helping.
The Supply Chain Problem
Most MCP servers in the wild aren’t written by the company whose API they wrap. They’re written by some third party. You install the server, you hand it your API credentials, and now a random GitHub repo has your Stripe key, your GitHub token, your database connection string.
We just watched a wave of supply chain attacks tear through npm and PyPI. Typosquatted packages, compromised maintainer accounts, malicious post-install scripts harvesting credentials and exfiltrating them to attacker-controlled servers. The MCP ecosystem is the same attack surface with a fresh coat of paint. New package registry. New install flow. Same gullible developers pasting npx some-mcp-server into their config and handing it the keys.
A binary the model wrote for you has the credentials you gave it, in code you can read, doing exactly what you asked. An MCP server from npm has whatever the maintainer felt like putting in there, updated whenever they push a new version, running as a long-lived process with access to your auth tokens. Pick which one you’d rather defend in a postmortem.
The Tooling Tax
Every MCP server is a thing to maintain. New auth scheme. New deployment target. New attack surface. New versioning story. You’re paying ongoing infrastructure cost so the model doesn’t have to write a binary it could write in 90 seconds.
The math doesn’t work.
What This Looks Like in a Year
I think the MCP ecosystem peaks and then gets quietly absorbed. The pattern that wins is: model reads docs, model writes binary, binary calls API, model invokes binary. No middle layer. No standardized protocol that’s really just JSON-RPC with extra steps. No fleet of servers to keep alive.
The model is the integration layer. It was always going to be.