Engineering 8 min read

You Can Call an MCP Server Without an LLM (It's Just JSON-RPC)

MCP is marketed as the way to give AI assistants access to your tools, so most people assume you need a model to use one. You don't. An MCP server is a JSON-RPC service with a discovery endpoint, and calling it directly is faster, deterministic and free. Here's when to skip the model entirely, when not to, and the whole client in about forty lines.

Updated Aug 2026
You Can Call an MCP Server Without an LLM (It's Just JSON-RPC)

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 #

Strip away the framing and an MCP server offers a very small surface:

MethodWhat it doesAnalogy
initializeVersion handshake, once per sessionOpening a connection
tools/listReturns every tool: name, description, JSON schemaAn OpenAPI document
tools/callRuns one tool with arguments, returns contentCalling 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.

Decision diagram: if you know which tool and arguments to use, call the MCP server directly; only involve a model when the sequence of calls must be decided at runtime Do you already know which tool + arguments? YES NO Call it directly deterministic · instant · free "fetch keywords for this ASIN" "pull yesterday's spend" Let a model drive it decides the sequence "why did ACOS jump last week?"

Nearly every scheduled job, sync and import 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 callThrough a model
LatencyOne HTTP round tripModel inference, plus the same round trip, plus a second inference to summarise
CostZero beyond the API itselfTokens in and out, on every single pull
DeterminismSame input, same call, alwaysMay pick different arguments, or a different tool, or paraphrase the result
DebuggabilityYour code chose the argumentsYou are debugging a decision you did not make
Failure modeAn exceptionA 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 #

  1. The response is an SSE frame, not JSON

    Even for a single reply, Streamable HTTP servers commonly return event: message followed by a data: line. Calling .json() on that fails. Parse the data line — the client above handles both shapes.

  2. 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 scopes. Many servers also support Dynamic Client Registration, meaning no application form at all.

  3. tools/list is your documentation

    Call 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, 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.

Don't want to build this yourself?

I set this up for sellers and agencies every week. Book a free 30-minute audit of your Amazon data & PPC setup — you'll leave with a plan either way, whether we work together or not.

Found this helpful? Share it:

WhatsApp