TL;DR: If you've ever wondered how an AI agent "magically" discovers tools exposed by an MCP server, forget the marketing diagrams for a moment. Grab curl, inspect the network traffic, and it all starts making sense.


When I first started reading about the Model Context Protocol (MCP), almost every explanation looked something like this:

User
   ↓
LLM
   ↓
MCP
   ↓
Tools

The explanation usually stopped at:

"The LLM discovers tools and calls them."

That wasn't satisfying.

I wanted to know:

  • Where are the tools actually exposed?
  • How does the agent know what tools exist?
  • Can I call an MCP server without an LLM?
  • Is an MCP server just another REST API?
  • What's actually happening on the wire?

So instead of reading more tutorials, I decided to reverse engineer a real MCP server.

I used LangChain's public MCP endpoint:

https://docs.langchain.com/mcp

Step 1: Ignore the LLM

The first thing I realized is that you don't need an LLM at all.

Using the Python MCP SDK:

tools = await session.list_tools()

The server replied with a list of tools.

This was my first "aha!" moment.

Unlike REST APIs, where developers typically read documentation to learn which endpoints exist, an MCP client simply asks the server:

tools/list

and the server replies with every available tool and its input schema.

Conceptually:

Client
   │
   │ tools/list
   ▼
MCP Server
   │
   ▼
Available tools

The MCP server itself becomes the source of truth.


Step 2: No LLM Is Required

Suppose the server advertises:

search_docs_by_lang_chain(query: string)

I can simply call it.

result = await session.call_tool(
    "search_docs_by_lang_chain",
    {
        "query": "How do I create a LangGraph agent?"
    }
)

No LLM.

No reasoning.

No AI.

Just:

Client
   │
tools/call
   │
   ▼
MCP Server
   │
Runs tool
   │
Returns result

This was probably the biggest realization:

MCP is fundamentally a protocol for discovering and invoking tools.

The LLM is just another client.


Step 3: Is the SDK Doing Something Magical?

The obvious next question was:

Is the Python SDK hiding some secret sauce?

Turns out...

Not really.

I replaced it with curl.

First, I listed the available tools.

curl -X POST https://docs.langchain.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc":"2.0",
    "id":1,
    "method":"tools/list"
  }'

The server immediately returned the list of tools.

Then I called one.

{
  "jsonrpc":"2.0",
  "id":2,
  "method":"tools/call",
  "params":{
    "name":"search_docs_by_lang_chain",
    "arguments":{
      "query":"How do I create a LangGraph agent?"
    }
  }
}

It worked.

The SDK wasn't performing magic.

It was simply wrapping the protocol.


Step 4: curl -v Was More Educational Than the Docs

Running with verbose output:

curl -v ...

revealed far more than I expected.

MCP Runs Over Plain HTTPS

The first surprise:

Connected to docs.langchain.com

SSL connection using TLSv1.3

HTTP/2

Nothing exotic.

Just HTTPS.


MCP Uses JSON-RPC

Instead of REST endpoints like:

GET /tools

POST /search

everything goes through a single endpoint:

POST /mcp

The operation is encoded inside the request body.

{
  "jsonrpc":"2.0",
  "method":"tools/list"
}

or

{
  "jsonrpc":"2.0",
  "method":"tools/call"
}

This is JSON-RPC.

JSON vs JSON-RPC

This confused me initially.

JSON is just structured data.

{
    "query":"LangGraph"
}

JSON-RPC is a way of calling remote functions.

{
    "jsonrpc":"2.0",
    "method":"tools/list",
    "params":{}
}

Think of it this way:

JSON
↓

A document
JSON-RPC
↓

A function call encoded as JSON

The Response Wasn't JSON

The response headers included:

content-type: text/event-stream

Instead of returning:

{
    "result": ...
}

the server returned:

event: message

data:
{
   "result": ...
}

This is Server-Sent Events (SSE).


What Is Server-Sent Events?

Most REST APIs work like this:

Request

↓

Response

↓

Connection closes

SSE keeps the connection alive.

Instead of one response, the server can stream many events.

event: message
data: {"progress":10}

event: message
data: {"progress":20}

event: message
data: {"progress":30}

This is why AI APIs often stream tokens, and why MCP uses SSE as its transport.

Even though tools/list only returned a single event, the protocol supports long-running tools that stream progress back to the client.


Step 5: So Where Does the LLM Come In?

Suppose the MCP server advertises:

search_docs(query)

fetch_page(url)

list_examples(topic)

A user asks:

"How do I create a LangGraph agent?"

Who decides to call search_docs?

Not the server.

Instead:

  1. The client asks for the available tools.
  2. The tool definitions are shown to the LLM.
  3. The LLM decides which tool best answers the user's question.
  4. The client sends tools/call.
  5. The MCP server executes the tool.

The MCP server never interprets natural language.

It simply executes requested tools.


The Mental Model That Finally Clicked

Initially I thought of MCP as:

AI magic

After spending an hour with Python, curl and the network inspector, I now think of it as:

HTTPS
    +
JSON-RPC
    +
Runtime Tool Discovery
    +
Tool Invocation

The LLM is not MCP.

The tools are not MCP.

MCP is simply the protocol that allows clients to discover and invoke capabilities exposed by a remote service.


The Biggest Surprise

The biggest surprise wasn't that MCP exists.

It was how ordinary it is.

The SDK wasn't doing anything magical.

The protocol wasn't doing anything magical.

Everything ultimately boiled down to a few JSON-RPC messages sent over HTTPS.

Sometimes the fastest way to understand a protocol isn't another tutorial.

It's opening curl -v.


What's Next?

There are still a few things I'd like to explore:

  • How stateful MCP sessions work.
  • Resources vs Tools vs Prompts.
  • Authentication.
  • Building a minimal MCP server from scratch.
  • How different transports (stdio vs HTTP) compare.

Those will probably become future posts.