The Model Context Protocol, explained for people who have to wire it up
What an MCP server is, how JSON-RPC tool calls work, the difference between stdio and Streamable HTTP transport, and what a client actually sends the model.
Last reviewed 2026-08-27 · View as Markdown
The Model Context Protocol explained in one paragraph
The Model Context Protocol (MCP) is an open specification for connecting an AI
application to something outside it — a file system, a database, a browser, a
project backlog — through a fixed set of JSON-RPC 2.0 methods. Strip the name
away and it is JSON-RPC tool integration for AI applications: the server
describes what it can do in JSON Schema, the client relays those descriptions
to a model, the model asks for a call, the client makes it and hands back the
result. The specification is versioned by date and has changed shape more than
once, so this guide covers what installed clients speak today and notes where
the current revision, 2026-07-28, differs. The rest is how MCP servers work
at the level of bytes on the wire, so that when a connection fails you can
tell which side is wrong.
MCP client and server architecture
The specification's architecture page defines three roles.
| Role | What it is | Examples |
|---|---|---|
| Host | The application the user is in. It creates clients, controls their lifecycle and permissions, holds the conversation, and enforces consent before a tool runs. | Claude Code, Cursor, Claude Desktop |
| Client | A connector inside the host, talking to exactly one server. Five configured servers means five clients. | Each entry under mcpServers in a config file |
| Server | A process or endpoint exposing resources, tools and prompts. It sees only what its client sends it. | @modelcontextprotocol/server-filesystem, https://autoplans.dev/api/v1/mcp |
Two of the design principles matter in practice. A server is not meant to read the whole conversation or see into other servers, so anything it needs — a project id, a file path — arrives as an argument on the call. And the host holds the model: a server wanting a completion, or a question put to the user, asks the client for it.
An MCP JSON-RPC example, message by message
Every message is a JSON-RPC 2.0 object. A request has an id; a notification
has none and must never be answered; a response carries the request's id and
either result or error. MCP tightens the base standard in one place: an
id must be a string or an integer, never null.
Shipping clients speak one of the revisions the specification now calls
legacy — 2024-11-05 through 2025-11-25 — where a connection opens with a
handshake:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ "protocolVersion":"2025-06-18", "capabilities":{}, "clientInfo":{"name":"ExampleClient","version":"1.0.0"}}}
The server answers with the version it will speak, its capabilities and its identity — this is the Autoplans server's reply:
{"jsonrpc":"2.0","id":1,"result":{ "protocolVersion":"2025-06-18", "capabilities":{"tools":{},"resources":{},"prompts":{}}, "serverInfo":{"name":"autoplans-mcp-server","version":"1.0.0"}}}
The client then sends notifications/initialized — no id, so no reply — and
asks for the catalogue with tools/list:
{"jsonrpc":"2.0","id":2,"result":{"tools":[ {"name":"get_task", "description":"Get details of a specific task including subtasks", "inputSchema":{"type":"object", "properties":{"taskId":{"type":"string","description":"Task UUID"}}, "required":["taskId"]}} ]}}
A call names a tool and passes arguments satisfying its schema. The answer is a list of content blocks:
{"jsonrpc":"2.0","id":3,"method":"tools/call", "params":{"name":"get_task","arguments":{"taskId":"..."}}}
{"jsonrpc":"2.0","id":3,"result":{ "content":[{"type":"text","text":"{\"success\":true,\"data\":{...}}"}]}}
Two kinds of failure look different on purpose. A JSON-RPC error — unknown
method -32601, unknown tool or malformed request -32602 — says the request
was wrong, and a model is unlikely to recover from one. A result with
isError: true says the tool ran and failed, and clients should pass that
text to the model so it can correct itself.
Revision 2026-07-28 removes the handshake. The protocol is stateless: every
request carries its own protocol version and client capabilities in a _meta
block under params, servers must implement server/discover for clients
that ask what is supported up front, and every result carries a resultType,
"complete" for a finished call. A dual-era client probes first and falls
back to initialize when the answer is not a recognised modern error.
MCP tools, resources and prompts
A server can offer three kinds of thing, distinguished by who decides to use them.
| Primitive | Controlled by | Methods | Typical use |
|---|---|---|---|
| Tools | The model | tools/list, tools/call | Read a task, create a file, run a query |
| Resources | The application | resources/list, resources/read, resources/templates/list | Read-only data the host attaches as context, keyed by URI |
| Prompts | The user | prompts/list, prompts/get | Templated messages, usually surfaced as slash commands |
Each is declared as a capability, and a server declaring tools must answer
tools/list. Tools carry almost all of the traffic, being the only primitive
the model reaches for by itself. The Autoplans server declares all three but
lists no resources and no prompts today; its tools are documented in
MCP tools.
MCP stdio vs HTTP transport
Protocol semantics are identical on every transport. A transport is a binding: it says how messages are framed and delivered, not what they mean.
stdio, short for standard I/O. The client launches the server as a
subprocess and writes one JSON-RPC message per line to its stdin; the server
writes one per line to stdout. Messages must not contain embedded newlines,
and nothing that is not a valid MCP message may go to stdout — a stray
console.log corrupts the stream. Logging goes to stderr, which the client
may capture or ignore and should not read as failure. Shutdown is closing
stdin, waiting, then forcing termination. There is no port, no TLS and no
credential beyond the process environment, which is why Claude Desktop's
claude_desktop_config.json describes a server as a command and its
args.
Streamable HTTP, introduced in 2025-03-26. The server exposes a single
endpoint that accepts POST. The client POSTs each message with an Accept
header listing both application/json and text/event-stream. The server
answers a request with either one JSON object or a Server-Sent Events stream
scoped to that request, carrying progress notifications and then the final
response; a notification gets an empty 202 Accepted. Revisions 2025-03-26
through 2025-11-25 also let the client GET the endpoint for server-initiated
messages and let the server mint an Mcp-Session-Id; 2026-07-28 removes
both and requires an MCP-Protocol-Version header and an Mcp-Method header on
every request, plus Mcp-Name on tools/call, resources/read and
prompts/get, so intermediaries can route without parsing the body.
HTTP+SSE, the 2024-11-05 design, is what older config files mean by
"sse". The client GETs an SSE endpoint, receives an endpoint event naming
a second URL, and POSTs there while server messages arrive on the open stream.
It has been deprecated since 2025-03-26; Claude Code's documentation says
the SSE transport is deprecated and to use HTTP servers where available.
| stdio | Streamable HTTP | |
|---|---|---|
| Where the server runs | On the user's machine, as a subprocess | Anywhere reachable by URL |
| Credentials | Environment variables | Authorization header |
| Batches (JSON arrays) | Allowed in 2025-03-26, removed in 2025-06-18 | Same |
| Failure you will see | Server printed to stdout; command not on PATH | 401; a proxy buffering SSE; 405 handled as an error |
Where the data lives decides which you write: a server wrapping a local repository is a stdio server, one fronting a hosted account is an HTTP server. For servers worth connecting to a coding agent, see MCP servers for developers.
A worked Streamable HTTP server: the Autoplans endpoint
Autoplans exposes an account's projects and tasks at
https://autoplans.dev/api/v1/mcp, authenticated with an API key. All of the
above is visible from curl:
curl -s https://autoplans.dev/api/v1/mcp \ -H 'content-type: application/json' \ -H "authorization: Bearer $AUTOPLANS_API_KEY" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -c 400
A valid key returns a result with a tools array of forty entries. The
endpoint:
- is stateless: it mints no
Mcp-Session-Id, and aGETcarryingAccept: text/event-streamanswers405withAllow: POST, there being no server-initiated stream to open; - acknowledges a body with no
id—notifications/initialized,notifications/cancelled— with an empty202, and treats a JSON array as a batch for clients on2025-03-26; - speaks
2024-11-05,2025-03-26and2025-06-18, echoing whichever the client asked for oninitialize; - answers
401to an unknown key,403when the plan does not include the hosted server, a JSON-RPC error naming the scope a key lacks (tasks:writeforupdate_task), and429withRetry-After; - relays upstream errors with their own status and message rather than flattening them, so a strict client reports the real reason a call failed.
The configuration block for each client is on the
MCP server page. Claude Code and Cursor
take the URL with a headers object; Claude Desktop,
whose config describes a server as a command to launch, uses the
autoplans-mcp npm package — a stdio server forwarding tools/list and
tools/call to the endpoint with AUTOPLANS_API_KEY.
What the client actually sends the model
After tools/list, the host holds the tool definitions — name, description,
JSON Schema — and attaches them to the request it makes to the model,
alongside the conversation. The model never talks to the server: it reads the
schemas and emits a structured request to call one; the host makes the
JSON-RPC call and appends the result as the next turn. Because the model API
is stateless, that payload is re-sent on every call, all session.
The specification asks servers to return tools in a deterministic order so
clients can cache the list and the provider's prompt cache keeps hitting.
Hosts economise too: Claude Code's tool search, on by default, loads only tool
names and server instructions at session start and defers the definitions
until needed, and its documentation says there is no fixed per-server tool cap
— the limit is your context budget. Not every host does this. Forty
Autoplans tool schemas on every model call is a large payload, and some
inference endpoints reject it outright; free endpoints in particular have
answered 503. That is why the Autoplans desktop app and CLI run a bundled
plugin exposing thirteen task-shaped tools instead. If your client fails in a
way that looks like the model choking rather than the server refusing, cut the
exposed tool set first; there is a longer note in
the MCP server documentation.
Authentication in practice: bearer keys against OAuth
Authorisation is optional in MCP, applies to HTTP transports, and is something
stdio servers are told not to implement — a local process takes credentials
from its environment. For HTTP the specification defines OAuth 2.1, with the
MCP server acting as resource server. An unauthenticated request
returns 401 with a WWW-Authenticate header pointing at protected-resource
metadata (RFC 9728); the client discovers the authorisation server from that,
obtains a client id, runs the browser flow with PKCE and a resource
parameter naming the MCP server (RFC 8707), then sends
Authorization: Bearer <token> on every request. Tokens must never go in the
query string, and an under-scoped token comes back as 403 naming the scopes
needed. Claude Code supports OAuth 2.0 for remote servers, and Cursor accepts
static OAuth client credentials in mcp.json.
The simpler arrangement, and where most hosted servers start, is a static API
key sent as a bearer token in the same header. It needs no discovery documents
and no callback port, it works from CI and from curl, and it is what the
Autoplans endpoint uses: create a key under Settings → API keys, give it
only the scopes the client needs, and paste it in — Claude Code takes one
through --header, Cursor through "headers". The costs are that the key
sits in a config file, so prefer an environment variable or an input prompt
where the client offers one, and that revoking it is manual. See
Accounts and API keys.
Questions
What is an MCP server, in one sentence?
A process or URL that answers JSON-RPC 2.0 requests such as tools/list and
tools/call according to the Model Context Protocol, so any compliant client
can discover and invoke what it offers without code written for that pairing.
Can one server serve several hosts at once?
Over Streamable HTTP, yes: one endpoint takes POSTs from many clients. Over stdio each host launches its own subprocess, so two editors on one machine run two copies.