# 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. Source: https://autoplans.dev/guides/model-context-protocol ## 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: ```json {"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: ```json {"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`: ```json {"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: ```json {"jsonrpc":"2.0","id":3,"method":"tools/call", "params":{"name":"get_task","arguments":{"taskId":"..."}}} ``` ```json {"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](/docs/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](/guides/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: ```bash 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 `id` — `notifications/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](/mcp-server). Claude Code and [Cursor](/mcp-server/cursor) take the URL with a `headers` object; [Claude Desktop](/mcp-server/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](/docs/mcp). ## 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 ` 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](/docs/accounts). ## 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.