The Assumption Worth Questioning #
A vendor you depend on ships an MCP server. The announcement is all about connecting Claude, ChatGPT and Cursor. Every tutorial shows a chat window. So when you need that data inside your own backend, the natural plan is: run a model on the server, give it the MCP connection, ask it to fetch the thing.
Stop there for a second.
In one line
MCP is a JSON-RPC protocol for listing and calling tools. Nothing in it requires a language model. If you already know which tool you want and what arguments it takes, call it directly — no model, no tokens, no non-determinism.
Models are the reason the standard exists — they need a uniform way to discover tools nobody hard-coded for them. But "designed for models" is not the same as "requires a model", and conflating those two costs you money and latency on every single request.
What an MCP Server Actually Exposes #
Update, October 2026: the MCP specification's current revision, 2026-07-28, drops the initialize handshake. Each request now declares its protocol version in its _meta field (and the MCP-Protocol-Version header over HTTP), and a client can call server/discover to see what a server supports. Servers built to revision 2025-11-25 or earlier still expect the handshake first, and the code below targets those servers, so it still works against them. Check which revision a server speaks before you drop the handshake.
Strip away the framing and an MCP server offers a very small surface:
| Method | What it does | Analogy |
|---|---|---|
initialize | Version handshake, once per session | Opening a connection |
tools/list | Returns every tool: name, description, JSON schema | An OpenAPI document |
tools/call | Runs one tool with arguments, returns content | Calling an endpoint |
Read tools/list as an endpoint list and tools/call as the request. That is genuinely all it is — a self-describing RPC API where the schema ships with the server instead of in a docs site that drifts out of date.
Which, incidentally, is a nice property to have even if you never involve a model: the tool schema is generated by the server, so it cannot go stale.
The Decision, In One Diagram #
The question is not "MCP or API". It is whether the choice of which call to make is known ahead of time.
Nearly every scheduled job, sync and import into a warehouse falls on the left. Nearly every open-ended question falls on the right. The mistake is putting the left-hand cases through the right-hand machinery because the vendor's marketing implied you had to.
What It Costs You to Get This Wrong #
Concretely, for a data pull where the tool and arguments are fixed:
| Direct call | Through a model | |
|---|---|---|
| Latency | One HTTP round trip | Model inference, plus the same round trip, plus a second inference to summarise |
| Cost | Zero beyond the API itself | Tokens in and out, on every single pull |
| Determinism | Same input, same call, always | May pick different arguments, or a different tool, or paraphrase the result |
| Debuggability | Your code chose the arguments | You are debugging a decision you did not make |
| Failure mode | An exception | A plausible-looking answer that is subtly wrong |
That last row is the one that matters. A direct call that fails throws. A model in the middle that misreads a schema can return something that looks entirely reasonable and is not, and you will not notice until the downstream data is wrong.
Non-determinism is not a small tax on a data pipeline. If the same ASIN can produce slightly different keyword sets on two runs because the model chose different arguments, you have made your own data unreproducible for no benefit.
The Whole Client #
Here is a complete, working MCP client. There is no SDK, because there does not need to be one.
import json, httpx
class MCPClient:
def __init__(self, url: str, token: str):
self.url = url
self.headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
# servers may answer either way; accept both
"Accept": "application/json, text/event-stream",
"User-Agent": "my-app/1.0",
}
self._id = 0
def _rpc(self, method: str, params: dict | None = None) -> dict:
self._id += 1
r = httpx.post(self.url, headers=self.headers, timeout=60, json={
"jsonrpc": "2.0", "id": self._id,
"method": method, "params": params or {},
})
r.raise_for_status()
data = self._parse(r.text)
if "error" in data:
raise RuntimeError(data["error"])
return data.get("result", {})
@staticmethod
def _parse(body: str) -> dict:
# Streamable HTTP returns SSE frames even for a single reply
if body.lstrip().startswith("{"):
return json.loads(body)
for line in body.splitlines():
if line.startswith("data:"):
return json.loads(line[5:].strip())
raise ValueError(f"unparseable MCP response: {body[:200]!r}")
def initialize(self):
return self._rpc("initialize", {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "my-app", "version": "1.0"},
})
def list_tools(self):
return self._rpc("tools/list").get("tools", [])
def call(self, name: str, **arguments):
return self._rpc("tools/call", {"name": name, "arguments": arguments})
Roughly forty lines, one dependency, and it works against any MCP server over Streamable HTTP.
Three Things That Will Trip You Up #
- The response is an SSE frame, not JSON
Even for a single reply, Streamable HTTP servers commonly return
event: messagefollowed by adata:line. Calling.json()on that fails. Parse the data line — the client above handles both shapes. - The auth is ordinary OAuth, discovered from well-known URLs
An MCP server that needs authentication publishes
/.well-known/oauth-protected-resource, which points at an authorisation server, which publishes its own metadata. Two unauthenticated GETs give you the endpoints and the exact scopes the server will accept. Many servers also support Dynamic Client Registration, meaning no application form at all. tools/listis your documentationCall it once and print it. The names, descriptions and argument schemas are all there, generated by the server. This is often more accurate than whatever docs page exists.
When a Model Genuinely Belongs There #
This is not an argument against agents. It is an argument against using one as an expensive HTTP client.
A model earns its place when the sequence of calls cannot be written in advance:
- Open-ended questions. "Which of my products lost organic rank this month, and does the ad spend explain it?" needs several tools, chosen based on what the earlier ones returned.
- Unfamiliar servers. Pointing an agent at a server nobody has integrated yet, to explore what is possible, is exactly what the discovery step is for.
- Turning structured data into prose. Fetch deterministically, then let the model write the summary, the listing copy from the product photos, the report.
The clean architecture is usually both: deterministic code fetches, the model reasons over what came back. Fetching is not the interesting part of what a model does, and it is the part it does worst.
A worked example: Helium 10 has no REST API — its MCP server is the only way in walks through discovery, OAuth, quotas and the SSE parsing against a real server, end to end.
FAQ #
Can you use an MCP server without an LLM?
Yes. MCP is a JSON-RPC protocol with three methods you need — initialize, tools/list and tools/call. Any HTTP client can speak it. A language model is only useful when the choice of which tool to call has to be made at runtime.
Is MCP just an API?
Effectively yes, with a built-in discovery step. tools/list returns each tool's name, description and JSON schema from the server itself, so the schema cannot drift from the implementation the way a separate docs site can.
Do I need an MCP SDK?
No. A complete client for Streamable HTTP is around forty lines: POST JSON-RPC, handle the SSE-framed response, and send a bearer token. An SDK is convenience, not a requirement.
Why does my MCP response fail to parse as JSON?
Because Streamable HTTP servers often return server-sent-event frames even for a single response — event: message followed by a data: line containing the JSON. Extract the data line before parsing, and send Accept: application/json, text/event-stream.
How does authentication work for a remote MCP server?
Standard OAuth 2.1. The server publishes /.well-known/oauth-protected-resource naming its authorisation server, which in turn publishes its endpoints, supported grants and scopes. Many also support Dynamic Client Registration, so you can obtain a client ID programmatically without an application process.
When should I put a model in front of an MCP server?
When the sequence of tool calls depends on intermediate results, when you are exploring an unfamiliar server, or when you want prose generated from the data. For a scheduled pull with known arguments, calling directly is faster, cheaper and reproducible.
Related guides
Claude & MCP
Helium 10 Has No Public API — Its MCP Server Is the Only Way In
If you have gone looking for a Helium 10 REST API to pull Cerebro or Magnet data into your own tool, you already know it does not exist. What does exist, since July 2026, is an MCP server — and you can call it straight from your own code, without Claude or ChatGPT anywhere in the loop. Here is the whole thing: discovery, OAuth, the tools, and the two gotchas that cost the most time.
Read guide →Claude & MCP
Writing Amazon Listing Copy From Product Photos with Claude Vision
Most AI listing tools start from a title and a few keywords, which means they describe a product nobody has looked at. Sending the actual product photographs to a vision model changes the output — it can see the material, the finish, the scale and what's in the shot. Here's the pipeline, the compliance gate that has to sit after it, and the parts that still need a human.
Read guide →Claude & MCP
Connect Claude to Amazon Ads and Seller Central (MCP Setup)
Amazon runs two MCP servers — one for Ads, one for the Selling Partner API — that let Claude query your accounts directly. The built-in connector flow fails with '400 Bad Request: An unknown scope was requested'. Here are the setup steps that work, then why the connector is broken and what the connection costs you in context.
Read guide →Running PPC across several accounts?
I built a dayparting control room that runs hourly across a PPC manager's client accounts: rules, dry runs, approvals and an audit log. Tell me what your tooling can't do and I'll quote the build at a fixed price.