MCP or a REST API: which one should your agent use to read the web?
MCP and a plain HTTP API do the same job in different places. When to expose a capability as a tool, when to call the endpoint, and why most teams run both.

The Model Context Protocol went from interesting to assumed in about a year. Data platforms, testing tools and research databases now ship an MCP server as a standard product surface, alongside the REST API they already had. If you are building an agent that reads the web, you now have two doors into the same capability and a decision to make about which one to walk through.
The honest answer is that they are not competitors. They sit at different layers, and the interesting question is not which is better but which one owns the decision.
The actual difference
A REST endpoint is something your code calls. You decide when to call it, with what arguments, and what to do with the response. The model never sees the endpoint; it sees whatever your code chose to put in the context.
An MCP tool is something the model calls. You register a capability, describe it, and hand the decision to the agent. It decides when reading a URL is the right next step.
That is the whole distinction, and everything else follows from it.
Your code decides -> call the HTTP endpoint
The model decides -> expose it as an MCP tool
When the model should not decide
Plenty of production paths are better off with the model out of the loop.
Fixed pipelines. A nightly job that reads two hundred known URLs and writes them to a store has no decision in it. Calling an endpoint in a loop is simpler, cheaper, easier to retry, and easier to reason about when it breaks at 3am. Giving that job an agent that chooses what to read adds a nondeterministic step to something that was deterministic.
Anything with a hard budget. When your code makes the call, you can count the calls. When the model makes the call, you get to watch a retry loop discover an interesting website. Budget enforcement belongs where the loop is, and if the loop is inside the model you need a different kind of guard.
Server-side work with no chat in it. Ingestion, enrichment, scheduled refreshes. There is no conversation to attach a tool to.
For all of these the shape is a normal HTTP call:
curl -s -X POST https://api.lyrenth.com/v1/aidocument \
-H "Authorization: Bearer $LYRENTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/pricing"}'
One URL in, one JSON document out, and your code decides what happens next.
When the model should decide
The opposite case is just as clear.
Research and open-ended questions. "Find out what changed in this library's licensing" is not a fixed list of URLs. The agent has to read one page, discover the next, and stop when it has enough. Making that a tool call is the whole point.
Interactive assistants. A person asks about a page they are looking at. The agent should be able to read it without you having predicted that turn in the conversation.
Multi-step work where reading is one of several options. If the agent can read, search a codebase, and run a query, those should be peers. The model chooses, and MCP is the vocabulary for offering the choice.
That is why the read capability is published as an MCP server as well as an endpoint: the same read, offered to whichever layer is making the decision.
Running both, which is the usual answer
Most real systems end up with both, and the split is generally clean.
- Ingestion path: HTTP. Known URLs, scheduled, budgeted, retried.
- Agent path: MCP. Unknown URLs, chosen live, bounded by the agent's own limits.
They share an index, so a page read once through either door is available cheaply through the other. That is the part worth designing for: the value is not the protocol, it is that the fetch already happened. A page in the index answers in milliseconds and costs a fraction of the tokens the raw HTML would have, because the extraction was done once rather than per caller.
Failure handling is where they really diverge
This is the part that decides how each one feels in production, and it is rarely in the comparison posts.
When your code calls an endpoint, you own the failure. A timeout, a refusal from the origin, a page with nothing readable on it: your code sees the status, decides whether to retry, and decides what the user is told. The failure is a branch in a program you wrote.
When the model calls a tool, the failure becomes part of the conversation. The tool returns an error, the model reads it, and the model decides what to do next. That is genuinely useful when the error is informative, because an agent that is told a page could not be read can go and find another source without being asked. It is genuinely dangerous when the error is vague, because the model will improvise, and improvising around a failed read means answering from memory instead of from the page.
The practical consequence: if you expose a read capability as a tool, spend the effort on the error text. "Not found" invites a guess. "That URL returned no readable content, the page draws its text with JavaScript" tells the agent something it can act on. Errors written for a log file are the wrong genre; write them for a reader who has to decide what to do next.
The same applies to limits. An HTTP 429 in a pipeline is a backoff. An HTTP 429 handed to an agent mid-conversation is a decision point, and the agent handles it well only if the message says how long to wait.
Three things that bite
A tool name is a public API. People pin your MCP tools in their agent configuration. Rename a tool and you break installs you cannot see and cannot fix. Treat tool names with the same seriousness as an endpoint path, which is to say: choose carefully and then stop changing it.
The description is the interface. With a REST endpoint your docs are read by a person who then writes code. With an MCP tool the description is read by the model, at decision time, every time. A vague description produces an agent that calls the tool at the wrong moments, and the fix is copywriting rather than code.
Credentials pass through, they do not disappear. An MCP server that reads the web on a user's behalf is still spending that user's quota. Make the key the caller's own, so usage and limits land on the right account. Anything else turns into an accounting problem the first week it is popular.
Choosing, in one table
| Situation | Use |
|---|---|
| Known URLs, scheduled | HTTP endpoint |
| Hard per-run budget | HTTP endpoint |
| No conversation in the loop | HTTP endpoint |
| Agent decides what to read next | MCP tool |
| Interactive assistant | MCP tool |
| Reading is one option among several | MCP tool |
| Both of the above, in one product | Both, sharing the index |
Start with whichever loop you already have
If you already have a pipeline, add the endpoint: it is a POST and a JSON response, and the quickstart is about a minute of work.
If you already have an agent, add the MCP server, because the decision is already living in the model and this just gives it a better way to read than driving a browser. We wrote about why that distinction matters in agentic browsers vs crawlers.
The wrong move is building a bespoke reading layer inside your agent framework, which is how teams end up maintaining an HTML parser they never wanted. Whichever door you pick, the thing on the other side should hand back a document, not a page.