Nobody is deprecating REST. An MCP server is usually just another client of the API you already shipped. Here is the call path, the wrapper code, and the token rule that bites teams.
Day 16 ended on one line: MCP standardizes the connection, not the tool. The question that follows within about ten seconds is whether that means the REST API you shipped last year is now legacy. It is not. MCP vs API is a layering question, and once you see where the boundary sits the rest of the decision gets easy. Your API keeps every job it has today, and the MCP server you write will spend most of its life as a client of that API.
The misreading is expensive in a specific way. A team hears "standard protocol for AI" and schedules a rewrite of integrations that were already correct. Two weeks later the API underneath is byte for byte identical, the agent still calls the same endpoints, and the only thing that changed is who calls them and how they were described. The rewrite was never the work.
Here is the part that usually unlocks it. MCP is itself an API. It is a JSON-RPC 2.0 API with a fixed method set, a fixed result envelope, and a defined authorization story. So the comparison is not API versus something that is not an API. It is a general purpose interface you designed for developers against one specific standardized interface designed for a model to consume at runtime.
An API answers: how do two applications call each other? MCP answers: how does a model discover, understand and safely invoke a tool it has never seen before?
| Your API | MCP | |
|---|---|---|
| Primary consumer | A developer who read your docs | A model choosing at runtime |
| Contract lives in | An OpenAPI file, a README, an SDK | A tools/list response on the wire |
| When the client learns the shape | At the time somebody wrote the code | At the time it connects |
| Surface design goal | Cover every operation completely | Cover the tasks a model should perform |
| Auth | Whatever you chose | OAuth 2.1 resource server on HTTP transports |
| Errors | Your status codes and body shapes | JSON-RPC errors plus isError: true results |
The clean test is who writes the integration. If a human reads your docs and writes the calling code ahead of time, an API is sufficient and MCP is overhead. If a model has to pick a call at runtime, from a set of capabilities that did not exist when its host application shipped, you need something that describes itself. That is the whole job MCP took.
MCP takes none of the following, and any explanation that suggests otherwise is wrong:
If your API is the system of record today, it stays the system of record. MCP sits above it and describes a subset of it in a shape a model can use.

Left to right: the agent decides, the MCP client speaks the protocol, the MCP server receives JSON-RPC, and then the server does ordinary backend work. It issues an HTTPS request to your REST API, or runs a SQL query, or calls a third party SaaS API. Nothing in that last hop is special. Your API sees a normal service client with a normal credential and a normal user agent. It never learns a model is on the other end, and it should not have to.
On Streamable HTTP the inbound call is one HTTP POST per JSON-RPC message. The 2026-07-28 revision mirrors two body fields into headers so gateways can route and meter without parsing the body:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: find_order
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call"
Mcp-Method is required on all requests and Mcp-Name is required on tools/call, resources/read and prompts/get. If a header disagrees with the body, the server must reject with 400 Bad Request and JSON-RPC error code -32020, HeaderMismatch. That rule exists because a load balancer routing on a header while the server executes on the body is a real security hole.
The practical payoff is that you can rate limit refund_order differently from find_order at the edge, without your gateway ever reading a JSON body. That is something a plain REST API gives you for free through the path, and it is something MCP had to add deliberately because every call goes to the same endpoint.
Install the official Python SDK. Version 2.x renamed the in-SDK FastMCP class to MCPServer and dropped the old mcp.server.fastmcp import path, so older snippets will not run.
uv add "mcp[cli]" httpxNow server.py. This wraps an existing orders REST API. Read it for two things: how little of it is protocol code, and where the credentials come from.
import os
from typing import Annotated, Literal
import httpx
from pydantic import Field
from mcp.server import MCPServer
from mcp.server.auth.middleware.auth_context import get_access_token
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings
from mcp.server.mcpserver.exceptions import ToolError
from
Count the protocol code in there. Two decorators and a run() call. Everything else is HTTP calls into an API that already existed, error mapping, and a scope check. That is the honest shape of most production MCP servers, and it is why "MCP replaces REST" never survives contact with an actual implementation.
Two details worth pausing on.
ToolError produces a normal successful JSON-RPC result carrying isError: true and your message in the content. That is what you want, because the model reads it and can act on it. An unhandled exception gets the caller a generic "Error executing tool" and teaches the model nothing. Use raise_for_status() for genuine faults you do not want the model retrying around, and ToolError for anything it could plausibly recover from.
refund_order makes three calls into the orders API. It is still one tool. Which brings us to the design mistake that costs more than the protocol decision.

The most common error is not choosing MCP over an API. It is mapping the API one to one onto tools. Forty endpoints become forty tools, the model gets forty descriptions competing for attention in one context window, and selection accuracy falls off a cliff. You have not standardized anything, you have pasted your OpenAPI file into the prompt with extra steps.
A tool is a task the model should be able to complete in one call. refund_order is a good tool even though it takes three internal API calls. patch_order_line_item_v2 is an endpoint wearing a tool costume.
Sizing rules that hold up:
find_order beats get_orders_query. The model is matching the user's goal against your name, not your resource hierarchy.get_order_by_id, get_order_by_email and search_orders are one tool with one flexible argument. Three near-identical descriptions is three chances to pick wrong.limit, return a compact result, and say in the text when there is more. Do not make the model manage a cursor unless it genuinely has to.There is a related question about generating tools straight from an OpenAPI document. The generators are genuinely useful for a first pass, and for a large internal API they will save you a day. They degrade for predictable reasons: an operation summary was written for a developer who also has the guide open, there is no field for "when not to use this", every operation becomes a tool because the spec has no opinion about which ones matter, and deeply nested request schemas expand into hundreds of tokens per tool. Generate, then cut hard and rewrite the descriptions by hand. The generated version is a draft, not a surface.

This is where MCP genuinely does add something your API does not have, and where the spec is unusually strict.
On HTTP transports a protected MCP server acts as an OAuth 2.1 resource server. The 2026-07-28 revision requires that MCP servers implement OAuth 2.0 Protected Resource Metadata (RFC 9728) so clients can discover the right authorization server, and clients must use that metadata for discovery. Clients must send the RFC 8707 resource parameter on both the authorization request and the token request, naming the canonical URI of the server they intend to call.
Then the rule that catches people. The spec says MCP servers must validate that access tokens were issued specifically for them as the intended audience, that they must only accept tokens valid for their own resources, and that they must not accept or transit any other tokens. Clients, in turn, must not send a server any token other than one issued by that server's authorization server.
So token passthrough is not a shortcut you are choosing, it is a spec violation and a confused deputy vulnerability. If your MCP server accepts whatever bearer token arrives and forwards it to a downstream API, any service that can get a token from a shared authorization server can now drive your server's privileges. That is the check the IntrospectingVerifier above is doing when it compares aud against RESOURCE_URL, and it is four lines that a lot of servers are missing.
Which means your MCP server has two credential planes, not one:
Nothing about MCP solves the outbound plane. That is the same per-tenant credential problem from Day 15: encrypted storage, serialized refresh, rotation, revocation, audit. The protocol tells you who is calling. Acting as that person against Salesforce is still yours.
Two more things the spec fixed that are easy to miss. Servers should signal missing permission through the challenge rather than a bare failure: a 401 with a WWW-Authenticate header carrying resource_metadata, or a 403 with error="insufficient_scope" and the scope needed for this operation.
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="orders:write",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"And on stdio, none of this applies. The spec says stdio implementations should not follow the HTTP authorization framework and should take credentials from the environment instead. That is reasonable when the server is a subprocess running as you. It is also exactly why the move from a local stdio server to a hosted HTTP one feels like a different project. It is a different project.
If you learned MCP before 2026-07-28, unlearn the handshake. The initialize exchange is gone, the Mcp-Session-Id header is gone from Streamable HTTP, and every request carries its own protocol version, client info and client capabilities in _meta. Any instance can serve any request.
Two consequences that change how you wrap an API.
First, tools/list must not vary per connection. It may vary by the authorization presented on the request, which is the supported way to give a caller with orders:read a smaller tool surface than a caller with orders:write. Vary it by anything connection-scoped and you have broken a guarantee clients now cache against, via ttlMs and cacheScope on the list result. If your list depends on the caller's scopes, cacheScope is private.
Second, stateful workflows need explicit handles. If a tool starts something that later calls finish, return an opaque identifier and take it back as an ordinary argument. The spec's guidance is worth following exactly: validate the caller's authorization against the handle on every call rather than treating the handle as a capability, keep it opaque, give it a bounded lifetime, state that lifetime in the tool description, and return an expiry as a tool error the model can recover from.
MCP splits failure into two channels, and getting this backwards silently costs you recoveries.
Protocol errors are JSON-RPC error responses. Unknown tool, malformed request, server fault. Clients may show these to the model, but recovery is unlikely.
Tool execution errors are successful JSON-RPC results with isError: true and a readable message in content. API failures, validation problems, business logic refusals. Clients should hand these to the model, because that is what enables self correction.
Return a -32603 for "customer not found" and you have hidden the only useful information from the only participant that could act on it. Return it as a tool error saying "no order matches ORD-10432, try list_recent_orders with the customer's email" and the model does the right thing on the next turn. Error messages on an MCP server are prompts. Write them for the reader you actually have.
Honest cases where a plain API wins:
Cases where it pays for itself:
Most production teams end up shipping both, which is the whole point. The API is the system. The MCP server is a model-facing façade over part of it.
403 with insufficient_scope and the exact scope neededtools/list order is deterministic and cacheScope is honest about whether the list varies by authorization-32020MCP does not replace your API. It standardizes the layer where a model meets it, and it leaves the API itself completely alone. The work that shows up when you adopt it is not rewriting endpoints. It is designing a tool surface a model can navigate, holding two credential planes apart, and running the server so it stays up for callers you do not control.
That last part is what we build at MewCP: hosted MCP servers, a gateway in front so an agent sees the eight tools that matter instead of ninety, and multi-tenant credential handling so tenant A never acts as tenant B. The protocol is the easy half. The operational half is where the quarters go.
Next: what actually lives inside an MCP server, and why tools, resources and prompts are three different things on purpose.