How does MCP work, message by message
MCP is JSON-RPC over stdio or HTTP. A client opens a session, asks what tools exist, then calls one. Here is each message, then a hosted server to point your agent at.
On this page
How does MCP work? A client opens a session with a server, asks it tools/list to find
out what the server can do, then sends tools/call when the model decides to use one of
them. Every message is JSON-RPC 2.0, carried over one of two transports: stdio for a local
subprocess, or Streamable HTTP for a hosted server. That is the entire protocol. The rest
of this post walks each step with the actual messages, then connects a real server so you
can watch the sequence happen.
The three roles you have to keep straight
The Model Context Protocol names three parties, and mixing them up is where most of the confusion starts:
- Host: the LLM application. Claude Code, Cursor, a chat UI you wrote yourself.
- Client: the connector inside the host. One client per server connection.
- Server: the thing exposing capabilities. A filesystem, a database, an email inbox.
Servers can offer three kinds of thing: tools (functions the model executes), resources (data pulled in for context) and prompts (templated workflows). Tools are the ones that do work, and they are what most servers ship. The spec takes its shape from the Language Server Protocol: write the integration once, and every host that speaks MCP gets it.
Step 1: the initialize handshake
A session starts with an initialize request. The client states the protocol version it
wants and what it can do. The server replies with the version it will actually use, plus
its own capabilities.
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}
The client then sends a notifications/initialized notification. It has no id, so there
is no response, and the session is live.
Over Streamable HTTP the server may return an Mcp-Session-Id header on that first
response. If it does, every later request has to carry it back, along with an
MCP-Protocol-Version header holding the negotiated version. Drop the session id and a
stateful server answers 400. If it answers 404, the session expired and you start again
from initialize.
Step 2: tools/list is the discovery call
The client hardcodes nothing. It asks:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
Back comes an array in which every tool carries a name, a human readable description,
and an inputSchema: JSON Schema describing the arguments. That schema and that
description are what the model actually reads, so a vague description is a tool the model
will misuse. Long lists paginate with a cursor. If the server declared the listChanged
capability it can push notifications/tools/list_changed and the client refetches, which
is how a server grows a new tool mid session.
Step 3: tools/call is where the work happens
When the model picks a tool, the client sends the name and the arguments:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "wait_for_email",
"arguments": { "inboxId": "ibx_123", "waitFor": "otp" }
}
}
The result is a content array (text, images, resource links, embedded resources) plus an
optional structuredContent object when the tool declares an outputSchema.
Error handling has two layers, and getting this wrong in your own server is common. A
protocol error, such as an unknown tool name or arguments that fail validation, comes
back as a JSON-RPC error object. A tool execution error, such as the API behind the
tool returning a 500, comes back as an ordinary result with isError: true. The
difference matters: the second one reaches the model as text it can read and act on, the
first one does not. Returning a protocol error for a business failure hides it from the
one participant that could have recovered from it.
Those four messages, initialize, notifications/initialized, tools/list and
tools/call, answer how does MCP work at runtime. Everything past them is a server
deciding what to expose and a model deciding what to call.
Step 4: connect a real server
Concretely: InboxDome runs a hosted MCP server at https://inboxdome.com/mcp over
Streamable HTTP, so there is nothing to install. In Claude Code:
claude mcp add --transport http inboxdome https://inboxdome.com/mcp \
--header "Authorization: Bearer idk_live_..."
Any other MCP client takes the same thing as JSON config:
{
"mcpServers": {
"inboxdome": {
"type": "http",
"url": "https://inboxdome.com/mcp",
"headers": { "Authorization": "Bearer idk_live_..." }
}
}
}
That tools/list now returns 7 tools: create_inbox, wait_for_email, list_messages,
get_message, run_assertions, delete_inbox and list_domains. Connecting with no key
at all still initializes and lists the tools, so an agent can inspect the server before it
has credentials. If you are still picking one, this breakdown of
which mail servers expose the most useful tools puts the
options side by side. The InboxDome MCP server setup guide covers the rest,
including where to get your key.
Step 5: send the messages by hand
Nothing about MCP needs a special client. Over HTTP it is a POST with a JSON body, and the
detail people trip on is the Accept header: the spec requires the client to list both
application/json and text/event-stream, because the server is allowed to answer with a
single JSON object or to open an SSE stream instead.
curl -X POST https://inboxdome.com/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
Read Mcp-Session-Id off the response headers, put it on a second POST carrying
"method":"tools/list", and you have done by hand exactly what your editor does at
startup. When a server misbehaves, this is the quickest way to learn whether the bug is in
the server or in your client wiring.
Where MCP stops
The protocol standardises discovery and invocation. It does not standardise auth, beyond saying that HTTP servers should authenticate their connections. It does not decide when the model calls a tool. It gives you no sandbox. And it treats tool descriptions and annotations as untrusted unless the server itself is trusted, which is why the spec tells hosts to keep a human able to deny a call.
For an agent testing email, the sharp edge is therefore not the protocol, it is the mailbox you point it at. A receive-only address that expires on its own limits what a runaway loop can do with it, which is the model InboxDome uses. For the practical version, see how an agent drives a full signup and OTP flow, or read what the Email Testing API does and the API reference if you would rather call it straight from a test suite.
Disclosure: InboxDome publishes this blog and makes the products it mentions. We describe what they do rather than promise results.
Key takeaways
- MCP is JSON-RPC 2.0 carried over one of two transports: stdio for a local subprocess, or Streamable HTTP for a hosted server.
- A session runs in a fixed order: initialize, notifications/initialized, tools/list, then tools/call.
- Nothing is hardcoded in the client. Tools are discovered at runtime and the inputSchema is the whole contract the model reads.
- Protocol errors come back as JSON-RPC error objects. Tool failures come back as normal results with isError set to true, so the model can see them and react.
- MCP standardises discovery and invocation. It does not standardise auth, it does not sandbox anything, and it does not decide when a tool gets called.
Sources
- Model Context Protocol specification (2025-06-18) — Roles, the JSON-RPC base protocol, and the server and client feature set.
- MCP transports (stdio and Streamable HTTP) — Session ids, the Accept header rule, and the protocol version header.
- MCP tools specification — tools/list, tools/call, inputSchema, and the two error mechanisms.
- JSON-RPC 2.0 specification — The request, response, and notification shapes MCP builds on.
Links last checked August 31, 2026.
Frequently asked questions
Is MCP the same thing as function calling?
No. Function calling is how a model asks for a tool inside one vendor's API. MCP is the wire protocol that lets any host discover and invoke tools from a separate process or server, so one integration works across every client that speaks it.
How does MCP work over HTTP instead of stdio?
Over stdio the client launches the server as a subprocess and exchanges newline delimited JSON on stdin and stdout. Over Streamable HTTP the server is just a URL: the client POSTs each message to a single endpoint, and the server answers with one JSON object or opens an SSE stream. In the HTTP case there is nothing to install locally.
What happens when a tool call fails?
There are two layers. An unknown tool name or malformed arguments returns a JSON-RPC error object. A failure inside the tool, such as an upstream API returning a 500, returns a normal result with isError set to true, so the model reads the failure text and can retry or change course.
Which clients can connect to an MCP server?
Claude Code, Cursor, and any other client that implements the specification. The InboxDome server uses Streamable HTTP, so any client that supports an HTTP transport plus an Authorization header can connect to it.