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.

RoleWhat it isExamples
HostThe 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
ClientA connector inside the host, talking to exactly one server. Five configured servers means five clients.Each entry under mcpServers in a config file
ServerA 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 legacy2024-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.

PrimitiveControlled byMethodsTypical use
ToolsThe modeltools/list, tools/callRead a task, create a file, run a query
ResourcesThe applicationresources/list, resources/read, resources/templates/listRead-only data the host attaches as context, keyed by URI
PromptsThe userprompts/list, prompts/getTemplated 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.

stdioStreamable HTTP
Where the server runsOn the user's machine, as a subprocessAnywhere reachable by URL
CredentialsEnvironment variablesAuthorization header
Batches (JSON arrays)Allowed in 2025-03-26, removed in 2025-06-18Same
Failure you will seeServer printed to stdout; command not on PATH401; 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 a GET carrying Accept: text/event-stream answers 405 with Allow: POST, there being no server-initiated stream to open;
  • acknowledges a body with no idnotifications/initialized, notifications/cancelled — with an empty 202, and treats a JSON array as a batch for clients on 2025-03-26;
  • speaks 2024-11-05, 2025-03-26 and 2025-06-18, echoing whichever the client asked for on initialize;
  • answers 401 to an unknown key, 403 when the plan does not include the hosted server, a JSON-RPC error naming the scope a key lacks (tasks:write for update_task), and 429 with Retry-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.

Give the agent a plan it can read.

Describe a project, get a backlog, connect it over MCP. Free, no card.

Start free