MCP before and after
The Model Context Protocol turned every external service into a tool call. Here's what changed, what broke, and what's coming next.
26 · feb 05(day zero) first draft. the protocol is moving fast and the security story is catching up. 26 · mar 11(1m 6d later) rewrote the attack surface section with real CVEs. added MCP Apps, OAuth 2.1, and the CLI/API/MCP trilemma.
Before MCP, connecting an AI tool to anything external meant writing a bespoke integration. Every IDE plugin, every API wrapper, every file-access layer — someone had to build it from scratch, for that specific tool, and maintain it when the tool changed. If you wanted Claude to talk to your database and Cursor to talk to the same database, you wrote two integrations. If you added a third tool, you wrote a third. The N-times-M problem that haunts every interoperability story.
MCP changed the equation. One protocol. One server per service. Every client speaks the same language.
This is the before and after.
1. The old world
Every AI coding tool had its own way of reaching external services. Cursor had its plugin system. GitHub Copilot had its extensions. Claude Desktop had nothing — it was a chat window with no arms.
If you wanted to give an AI access to your Git history, you had a few options:
Paste terminal output into the chat (copy, switch windows, paste, repeat)
Write a wrapper script that formatted output for the model's context window
Build a native plugin for that specific tool, using that tool's specific API
Option three was the "right" answer, but the economics were brutal. Every integration was a snowflake. A Slack integration for Cursor shared zero code with a Slack integration for Claude. The companies building AI tools spent engineering time on integrations instead of on the model layer. Third-party developers had to pick which tool to support because supporting all of them was a full-time job.
The result was predictable: each tool had great integration with two or three services and terrible integration with everything else.
2. The new world
MCP flipped the architecture. Instead of N clients each implementing M integrations (N * M total), you write one MCP server per service and every client connects to it. The server exposes tools, resources, and prompts through a standard JSON-RPC protocol. The client discovers what's available and calls it.
A comparison:
Native integration MCP server
Protocol Tool-specific API JSON-RPC over stdio or HTTP
Auth Per-tool, bespoke OAuth 2.1 (standardized)
Discovery Hardcoded Dynamic tool listing
Portability Zero — locked to one client Any MCP-compliant client
Maintenance Two codebases to update One server, many clients
A Postgres MCP server exposes query, list-tables, describe-schema as tools. Claude Code connects to it. Cursor connects to the same server. A custom agent you built last weekend connects to it. Same server process, same tool definitions, same behavior.
Inherited, not magicClaude Code inherits your shell permissions. It runs as your user, in your terminal, with access to your files, your environment variables, your SSH keys. MCP servers it connects to run under the same user. There is no sandbox. If you can rm -rf /, Claude Code can too — it just needs you to approve the command (or to have granted blanket permission via --dangerously-skip-permissions).
This is a feature, not a bug. The alternative — a sandboxed environment with no real access — would make the tool useless for actual development work. But it means the security boundary is your judgment about what to approve.
Projects became portable
An .mcp.json file at the root of a repo declares which MCP servers a project needs. Clone the repo, and the AI tool reads the config and connects to the right servers. A project that needs GitHub, Postgres, and a custom internal API declares all three, and any team member's client picks them up automatically.
Portable attack surfacePortability cuts both ways. If a malicious .mcp.json lands in a repo — through a PR, a dependency, a compromised fork — every developer who opens that repo with an MCP-aware tool connects to the attacker's servers. The servers look normal. The tool descriptions say they do normal things. But the descriptions are the attack surface, and they are controlled by the server author. More on this in the attack surface section.
3. The inversion
The paradigm shiftBefore MCP, tools were dumb and integrations were smart. You built intelligence into each connector — parsing, formatting, error handling, retry logic — because the tool on the other end just exposed raw functionality.
After MCP, the model is the integration layer. The server exposes capabilities with natural-language descriptions. The model reads those descriptions and decides how to compose them. The "glue code" that used to live in thousands of lines of integration logic now lives in the model's reasoning. Tool descriptions became the API documentation, the SDK, and the user manual — all at once.
This is powerful and dangerous for the same reason. When the model trusts tool descriptions to decide what to do, anyone who controls the description controls the model's behavior.
4. The attack surface
MCP's simplicity is also its vulnerability. A server is a process that exposes tool definitions — name, description, input schema — over JSON-RPC. The client trusts those definitions to decide what to call and how. Three classes of attack exploit this trust.
Tool poisoning
The tool description is free-form text. The model reads it. The model trusts it.
In April 2025, Invariant Labs demonstrated a cross-server exfiltration attack using WhatsApp MCP. The setup: a user installs a benign-looking MCP server (a trivia game, say) alongside the WhatsApp MCP server. The trivia server's tool description contains hidden instructions — invisible in the client UI but visible to the model — telling it to read the user's WhatsApp message history and send it to an attacker-controlled number.
The model follows the instructions because they look like part of the tool's documentation. The data leaves through WhatsApp itself, which means it bypasses every network-layer DLP system because it looks like a normal outgoing message. The user approved "send a WhatsApp message." They did not approve "send my entire message history to a stranger."
Supply chain
CVE-2025-6514. CVSS 9.6. The mcp-remote npm package — a proxy that lets local MCP clients connect to remote servers over HTTP — had a command injection vulnerability in its authorization callback URL handling. A malicious remote MCP server could craft an OAuth redirect that triggered arbitrary OS command execution on the client machine. Full RCE from a remote server, on the developer's workstation, triggered by connecting to the server.
The package had over 437,000 total downloads and ~226,000 weekly downloads at the time of disclosure1. The vulnerability existed from version 0.0.5 through 0.1.15. The fix was a single function — sanitizeUrl in utils.ts — that wasn't sanitizing.
1The first documented case of full remote code execution on a client OS from a remote MCP server. Fixed in mcp-remote 0.1.16.
Rug pull
The most insidious pattern. A server passes review with clean, honest tool descriptions. Users approve it. Weeks later, the server pushes updated tool definitions with poisoned descriptions. The name stays the same. The input schema stays the same. The description — the part the model reads to decide behavior — changes.
MCP has no mechanism to alert the user that a tool definition changed after initial approval. The server controls the definitions. The client re-fetches them on each session. A tool you trusted on Monday can be a different tool on Tuesday, and nothing in the protocol tells you.
The .mcp.json vector makes this worse. A project configuration file points to a remote server URL. The server operator can change tool definitions at any time. Every developer using that config gets the new definitions on their next session. One compromised server definition, propagated through a shared config, silently reaching every machine that opens the repo.
No silver bullet yetThese three vectors — poisoning, supply chain, rug pull — are not theoretical. They have been demonstrated, exploited, and in the case of CVE-2025-6514, shipped in production packages. The MCP specification does not yet include mandatory integrity checks for tool definitions, server identity verification, or change-detection mechanisms. The burden is on the user to verify what they connect to.
5. MCP in 2026
The protocol didn't stand still. Two major additions landed, and the governance structure changed fundamentally.
Agentic AI Foundation
On December 9, 2025, the Linux Foundation announced the Agentic AI Foundation (AAIF). Anthropic donated MCP to the foundation. OpenAI contributed AGENTS.md. Block contributed goose. Platinum members: AWS, Anthropic, Block, Bloomberg, Cloudflare, Google, Microsoft, OpenAI.
This matters because MCP is no longer Anthropic's protocol — it's an industry standard governed by a neutral foundation. The same organizational structure that gave us Linux, Kubernetes, and Node.js. Whether that governance produces faster security standards or slower-moving committees remains to be seen, but the "one company controls the spec" risk is gone.
By early 2026, MCP crossed 97 million monthly SDK downloads across Python and TypeScript. Over 5,800 MCP servers and 300+ clients exist in the ecosystem. First-class support in Claude, ChatGPT, Cursor, Gemini, Microsoft Copilot, and VS Code.
MCP Apps
Announced January 26, 2026. The first official MCP extension. Before Apps, a tool could only return text — structured data that the model interpreted and summarized for the user. Apps let tools return interactive user interfaces rendered directly in the conversation.
The pattern: a tool declares a UI resource via the ui:// scheme, and the host renders it in a sandboxed iframe.
TYPESCRIPT Copy
registerAppTool(server, "show-analytics", { title: "Show Analytics", description: "Interactive analytics dashboard.", inputSchema: { dateRange: { type: "string" } }, _meta: { ui: { resourceUri: "ui://analytics/dashboard.html" } }, }, async ({ dateRange }) => { const data = await queryMetrics(dateRange); return { content: [{ type: "text", text: JSON.stringify(data) }] }; });
The tool returns data and a UI to display it. The iframe is sandboxed — no access to the parent window, no arbitrary network requests. All communication between the UI and the host goes through loggable JSON-RPC messages. The HTML content is pre-declared as templates, so hosts can review it before rendering.
Supported in Claude, Claude Desktop, VS Code Insiders, Goose, Postman, and MCPJam as of March 2026.
OAuth 2.1
The original MCP spec had no standardized authentication. Servers handled auth however they wanted — API keys in environment variables, custom token exchanges, or nothing at all. The March 2025 revision introduced OAuth 2.1 with PKCE as the standard authorization mechanism.
A server that requires authentication returns a 401 with metadata pointing to its authorization endpoint:
HTTP Copy
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="mcp", resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
The client discovers the authorization server through RFC 9728 (Protected Resource Metadata), performs the OAuth flow with PKCE1, and uses the resulting token for subsequent requests.
1PKCE — Proof Key for Code Exchange — is mandatory for every MCP client. Many MCP clients run in environments where storing secrets securely is difficult: containers, serverless functions, developer laptops with shared dotfiles.
The November 2025 revision went further: Client ID Metadata Documents replaced manual registration, and PKCE became non-negotiable for every client type. The authorization server is now formally split from the MCP server itself, so a single OAuth provider (your company's Entra ID, Okta, Auth0) can protect multiple MCP servers without each one reimplementing auth.
6. The trilemma — CLI, API, MCP
Three ways to connect an AI agent to external services. Each solves a different problem. Using the wrong one costs real tokens, real latency, or real engineering time.
The three paths
A CLI wrapper is a shell command. Input goes in, output comes out. playwright-cli screenshot https://example.com → returns a PNG. The AI calls it through Bash like any other command. No protocol, no discovery, no auth dance. The tool definition costs almost zero context window — it's just a command name and its flags.
An API call is a direct HTTP request to a service. POST /v1/screenshots with a JSON body, get a JSON response. The AI needs the endpoint URL, the auth token, and the schema. More structured than a CLI, more portable across languages, but each service has its own API design and its own auth mechanism. If you need two services, you learn two APIs.
An MCP server is a process that wraps a service in a standard protocol. It exposes tools — name, description, input schema — over JSON-RPC. The AI discovers what's available, reads the descriptions, and decides what to call. One protocol for everything. But those tool definitions live in the context window, and they're not free1.
1A single MCP server with 8 tools can consume 800-2,000 tokens in tool definitions alone. Five servers with 8 tools each = 4,000-10,000 tokens before the model does anything useful. On a 200K context window that's ~5%. On a 32K window it's ~30%.
When each one wins
CLI wrapper Direct API MCP server
Setup cost Minutes. Write a script. Hours. Learn the API. Minutes. Install the server.
Context cost Near zero Low (one endpoint) High (N tool definitions)
Discovery None — you hardcode it Manual — read the docs Automatic — the model reads tools
Portability Zero — your script, your machine Zero — your integration Total — any MCP client
Auth Your env vars Per-API (OAuth, API keys, etc.) OAuth 2.1 (standardized but heavy)
Best for One task, one team, tight scope Backend pipelines, server-to-server Open ecosystems, multiple clients
When each one fails
CLI when you need MCP. You write a github-cli wrapper for your team. It works. Then another team wants it. Then a third. Now you're maintaining three copies of the same script with different flags, different output formats, different assumptions about the environment. This is the N×M problem that MCP was built to solve.
MCP when you need CLI. You install the Playwright MCP server to automate a browser. It exposes 47 tools — navigation, screenshots, clicks, form fills, network interception, console capture, PDF export, accessibility audit. Your agent needs three of them. But all 47 tool definitions load into the context window, and the model has to reason about which ones to use. Garry Tan, CEO of Y Combinator, hit exactly this wall — wrote a 100-line CLI wrapper in 30 minutes that did the job better1.
1Garry Tan on X, March 2026: "MCP sucks honestly. It eats too much context window and you have to toggle it on and off and the auth sucks. I got sick of Claude in Chrome via MCP and vibe coded a CLI wrapper for Playwright tonight in 30 minutes... it worked 100x better and was like 100LOC as a CLI."
API when you need MCP. You call the GitHub API directly from your agent. Works fine — for GitHub. Then you add Postgres. Then Slack. Then Notion. Each API has its own auth, its own pagination, its own error codes. You're writing bespoke integrations for each one. If a second AI tool needs the same access, you write everything again. You're back to snowflakes.
MCP when you need API. You need your backend to query a database on every request. A direct SQL call takes 3ms. Routing it through an MCP server adds a process boundary, a JSON-RPC roundtrip, and protocol overhead — for something that was a single function call. MCP is designed for AI agents that need to discover and compose tools. If you already know exactly what to call, the protocol is pure overhead.
The selection ruleIf only one agent on one machine needs the tool, and you know exactly what it should do → CLI.
If your backend code needs the service and no model is involved in the decision → API.
If multiple clients need to discover and use the service, and the model decides when and how to call it → MCP.
The most common mistake in early 2026 is using MCP for everything because it's the new thing. Denis Yarats, CTO of Perplexity, moved his team away from MCPs and back to APIs and CLIs for internal tooling. The protocol solved the interoperability problem. But not every problem is an interoperability problem.
7. The fossil record
It would have been easy to write this article six months ago and conclude that MCP was a nice idea with fatal security flaws. A protocol where the model trusts tool descriptions, where servers can change definitions silently, where a single npm package can give an attacker full RCE on your machine.
But that's not what happened. The protocol absorbed the hits and kept growing. CVE-2025-6514 was patched. Tool poisoning was documented and mitigated by vendors adding description-change alerts1. OAuth 2.1 replaced the authentication free-for-all. The Linux Foundation gave it governance. 97 million monthly downloads suggest the developer community voted with their npm install.
1MCPhound and similar tools now hash tool definitions on first connection and alert on any subsequent change. Not part of the spec, but the ecosystem is filling the gaps.
The security problems are real. The attack vectors are documented. But MCP did something that no previous AI integration protocol managed: it got adopted. By everyone. Simultaneously. And now the security story has to catch up to the adoption curve, not the other way around.
That race is the one worth watching.