Model Context Protocol (MCP)
MCP is an open standard that lets a single agent connect to external tools, data, and prompt templates over a stateful JSON-RPC session. This page covers the host/client/server architecture, the tools/resources/prompts primitives, the initialize handshake and capability negotiation, the stdio and Streamable HTTP transports, and the exact wire format — with spec-accurate annotated JSON-RPC examples.
The MCP (Model Context Protocol) is an open standard for connecting LLM applications to the external data and tools they need. Anthropic introduced it on 25 November 2024 as "a new standard for connecting AI assistants to the systems where data lives, including content repositories, business tools, and development environments," positioning it as a way to replace "fragmented integrations with a single protocol." The original release shipped three things together: the protocol specification and SDKs, local server support in the Claude Desktop app, and an open-source repository of pre-built servers (Google Drive, Slack, GitHub, Git, Postgres, and Puppeteer were among the first).
The motivating problem is the M×N integration explosion: with M AI applications and N data sources, the pre-MCP world required up to M×N bespoke connectors. MCP collapses that to M+N — each application speaks MCP once, and each data source exposes MCP once. The spec draws an explicit analogy to the Language Server Protocol (LSP): just as LSP standardized how editors talk to language tooling, MCP standardizes how AI applications integrate context and tools.
This page describes the 2025-06-18 revision of the specification, which is the version a
client advertises as its protocolVersion. The protocol evolves on a dated-revision cadence;
the wire details below (method names, field names, transports) are taken directly from that
revision's spec.
Where MCP is an agent reaching down to tools and data, the A2A protocol is an agent reaching across to peer agents. See protocol comparison for how they relate, and tool use for the model-side mechanics that MCP feeds.
Architecture: host, client, server
MCP follows a client-host-server architecture. The three roles have precise meanings in the spec, and conflating them is the single most common source of confusion.
- The MCP host is the LLM application that the user interacts with — an AI IDE, a chat app like Claude Desktop, or a custom agent runtime. The host is the container and coordinator: it creates and manages client instances, controls connection permissions and lifecycle, enforces security and consent policies, coordinates the LLM/sampling integration, and aggregates context across all of its clients. Crucially, the full conversation history stays with the host.
- An MCP client is a connector inside the host. Each client maintains exactly one stateful session with one server — a strict 1:1 relationship. The client handles protocol negotiation, routes JSON-RPC messages bidirectionally, manages subscriptions and notifications, and maintains the security boundary for its server. A host with three servers runs three clients.
- An MCP server is a service that exposes capabilities. Servers expose resources, tools, and prompts via MCP primitives; operate independently with a focused responsibility; can run as a local subprocess or a remote service; and can request sampling, elicitation, or roots back through the client.
flowchart LR
subgraph host["Application Host Process"]
H[Host]
C1[Client 1]
C2[Client 2]
C3[Client 3]
H --> C1
H --> C2
H --> C3
end
subgraph local["Local machine"]
S1["Server 1
Files & Git"]
S2["Server 2
Database"]
C1 --> S1
C2 --> S2
end
subgraph net["Internet"]
S3["Server 3
External API"]
C3 --> S3
end
Three design principles shape this split. Servers should be extremely easy to build — the host carries the orchestration burden so a server only implements its narrow capability. Servers should be highly composable — many focused servers combine through the shared protocol. And servers must not be able to read the whole conversation, nor "see into" other servers — each connection is isolated, full history stays with the host, and the host enforces the boundary. This isolation is a deliberate security property, not an accident of design.
Primitives: what a server (or client) offers
MCP defines a small set of named primitives. Server primitives are offered to the client; client primitives are offered to the server. Each primitive is only usable in a session if the offering side declared the matching capability during initialization (see capability-negotiation">capability negotiation).
Server features:
- Resources — context and data for the user or model to use (files, database schemas, documents). Each mcp-resource is identified by a URI and is application-driven: the host decides how/whether to surface it.
- Prompts — templated messages and workflows. An mcp-prompt is user-controlled: it is meant to be explicitly invoked by the user, often surfaced as a slash command.
- Tools — functions the model can execute. An mcp-tool is model-controlled: the LLM discovers and decides to call it based on context (though the spec strongly recommends keeping a human in the loop with the ability to deny any invocation).
Client features (server-initiated, routed through the client to the host):
- Sampling — the server asks the host's LLM to generate a completion, enabling agentic and recursive behaviors. The protocol intentionally limits the server's visibility into the prompt, and the spec requires explicit user approval of sampling requests.
- Roots — the server inquires which filesystem/URI boundaries it may operate within.
- Elicitation — the server requests additional information from the user mid-session.
These three client features are why MCP is a bidirectional protocol rather than a simple request/response API: a server is not just answering calls, it can originate requests back toward the model and user.
The wire layer: JSON-RPC 2.0
Every message between client and server is a JSON-RPC 2.0 message, UTF-8 encoded. JSON-RPC
gives MCP its envelope; MCP defines the method names and params/result shapes that ride
inside it. There are exactly three message kinds:
- Request — has
jsonrpc: "2.0", anid(string or integer), amethod, and optionalparams. Unlike base JSON-RPC, theidMUST NOT benull, and it MUST NOT reuse an id already seen in the session. - Response — echoes the request's
idand carries either aresultobject or anerrorobject ({ code, message, data? }), never both. - Notification — has
jsonrpc, amethod, and optionalparams, but noid. It is one-way: the receiver must not reply.
Note one revision-specific detail: the 2025-06-18 spec removed support for JSON-RPC batching (arrays of messages in a single payload) that earlier revisions had allowed. Each HTTP payload now carries a single JSON-RPC message.
MCP also reserves a _meta field on requests/params for protocol-level and implementation
metadata, with key-name prefixes such as modelcontextprotocol.io/ and mcp.dev/ reserved for
MCP's own use.
Lifecycle: initialize, operate, shut down
An MCP session is stateful and moves through three phases.
1. Initialization
The initialize handshake MUST be the first interaction. The client sends an initialize
request declaring the protocol version it supports, its capabilities, and its implementation
info (clientInfo). The server MUST respond with its own protocolVersion, capabilities,
serverInfo, and optional instructions. The client then sends a notifications/initialized
notification to signal it is ready for normal operation.
sequenceDiagram
participant Host
participant Client as MCP Client
participant Server as MCP Server
Host->>Client: create client, start session
Client->>Server: initialize (protocolVersion, capabilities, clientInfo)
Server-->>Client: result (protocolVersion, capabilities, serverInfo, instructions?)
Client--)Server: notifications/initialized
Note over Client,Server: session is now in the operation phase
Before the server has responded to initialize, the client SHOULD NOT send other requests
(except ping); before receiving notifications/initialized, the server SHOULD NOT send
requests other than ping and logging.
Version negotiation: the client sends the latest version it supports. If the server supports
that version it MUST echo the same string back; otherwise it MUST respond with another
version it supports (its latest). If the client can't accept the server's choice, it SHOULD
disconnect. Over HTTP, the negotiated version is then pinned on every subsequent request via the
MCP-Protocol-Version header.
Here is a spec-accurate initialize request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18", // latest revision the client speaks
"capabilities": { // what THIS client can do for the server
"roots": { "listChanged": true },// can provide filesystem roots + notify on change
"sampling": {}, // can service server-initiated LLM sampling
"elicitation": {} // can relay server requests for user input
},
"clientInfo": {
"name": "ExampleClient",
"title": "Example Client Display Name",
"version": "1.0.0"
}
}
}
And the matching initialize result — the server advertises its capabilities so the client knows which primitives and sub-features are available:
{
"jsonrpc": "2.0",
"id": 1, // echoes the request id
"result": {
"protocolVersion": "2025-06-18", // server agrees on the version
"capabilities": {
"logging": {},
"prompts": { "listChanged": true }, // offers prompts; will notify on list changes
"resources": { "subscribe": true, // resources can be individually subscribed
"listChanged": true }, // and the list emits change notifications
"tools": { "listChanged": true } // offers tools; will notify on list changes
},
"serverInfo": {
"name": "ExampleServer",
"title": "Example Server Display Name",
"version": "1.0.0"
},
"instructions": "Optional instructions for the client"
}
}
The client then completes the handshake with the parameterless notification:
{ "jsonrpc": "2.0", "method": "notifications/initialized" }
If the version cannot be reconciled, the server returns a standard JSON-RPC error (code
-32602, "Unsupported protocol version") whose data.supported lists the versions it supports
(the data is an object, e.g. { "supported": ["2024-11-05"], "requested": "1.0.0" }).
2. Operation
Both sides exchange messages constrained by the negotiated capabilities — they MUST respect the
agreed protocol version and MUST only use capabilities that were successfully negotiated. This is
where tools/list, tools/call, resources/read, prompts/get, server-initiated sampling,
and the various notifications flow.
3. Shutdown
There are no dedicated shutdown messages; the transport signals termination. For stdio the
client closes the server's stdin, waits, then escalates to SIGTERM and finally SIGKILL if
needed; the server may instead close its stdout and exit. For HTTP, shutdown is simply closing
the HTTP connection(s). Implementations SHOULD also set per-request timeouts and, on timeout,
send a cancellation notification.
Capability negotiation
Capability negotiation is the mechanism that makes MCP extensible without versioning every
feature. During initialize, each side declares a capability object; a feature is only available
in the session if its capability was declared. The key capabilities:
| Side | Capability | Meaning |
|---|---|---|
| Client | roots |
can provide filesystem roots |
| Client | sampling |
supports server-initiated LLM sampling |
| Client | elicitation |
supports server requests for user input |
| Server | prompts |
offers prompt templates |
| Server | resources |
provides readable resources |
| Server | tools |
exposes callable tools |
| Server | logging |
emits structured log messages |
| Server | completions |
supports argument autocompletion |
Capability objects carry sub-capabilities: listChanged (the side will emit a list-changed
notification, available for prompts, resources, and tools) and subscribe (resources only — the
client may subscribe to changes of an individual resource). Both parties must honor declared
capabilities for the whole session.
Tools: discovery and invocation
A mcp-tool is a named, schema-described function the model can call. Discovery
uses tools/list (paginated via an opaque cursor / nextCursor); invocation uses
tools/call. A tool definition carries name, optional title, description, an inputSchema
(JSON Schema for arguments), an optional outputSchema, and optional annotations (which
clients MUST treat as untrusted unless the server is trusted).
tools/list request and result:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": { "cursor": "optional-cursor-value" } }
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_weather", // unique id used by tools/call
"title": "Weather Information Provider",
"description": "Get current weather information for a location",
"inputSchema": { // JSON Schema; arguments validated against this
"type": "object",
"properties": {
"location": { "type": "string", "description": "City name or zip code" }
},
"required": ["location"]
}
}
],
"nextCursor": "next-page-cursor" // present only if more pages remain
}
}
tools/call round trip — the model picks a tool by name, the client sends arguments
matching the inputSchema, and the server returns a result whose content is an array of typed
blocks (text, image, audio, resource_link, or embedded resource):
sequenceDiagram
participant LLM
participant Client as MCP Client
participant Server as MCP Server
Note over Client,Server: discovery (once)
Client->>Server: tools/list
Server-->>Client: tools[], nextCursor?
Note over LLM,Client: the model decides to act
LLM->>Client: select get_weather { location }
Client->>Server: tools/call (name, arguments)
Server-->>Client: result { content[], isError }
Client->>LLM: feed content back as observation
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather", // a tool advertised by tools/list
"arguments": { "location": "New York" } // validated against inputSchema
}
}
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [ // unstructured result: array of typed content blocks
{
"type": "text",
"text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
}
],
"isError": false // false = tool ran; true = tool-level (not protocol) error
}
}
MCP distinguishes two error channels for tools, and the distinction matters for agent
behavior. A protocol error (unknown tool, invalid arguments) comes back as a JSON-RPC
error object — the call never really ran. A tool execution error (an API failed, the input
was bad) comes back as a normal result with isError: true, so the failure text lands in the
model's context where it can react and retry. When a tool declares an outputSchema, the server
MUST also return a structuredContent object conforming to that schema (and SHOULD mirror it
as serialized JSON in a text block for backward compatibility). When the tool list changes, a
server that declared listChanged sends notifications/tools/list_changed.
Resources: addressable context
A mcp-resource is data identified by a URI. Discovery is resources/list;
retrieval is resources/read. Parameterized resources are exposed via
resources/templates/list (RFC 6570 URI templates). If the server declared subscribe, a client
can call resources/subscribe (and resources/unsubscribe) to receive
notifications/resources/updated when a specific resource changes; notifications/resources/list_changed
covers changes to the list itself.
{ "jsonrpc": "2.0", "id": 2, "method": "resources/read", "params": { "uri": "file:///project/src/main.rs" } }
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": [
{
"uri": "file:///project/src/main.rs",
"mimeType": "text/x-rust",
"text": "fn main() {\n println!(\"Hello world!\");\n}"
}
]
}
}
Resource contents are either text or base64 blob. A resource definition carries uri,
name, optional title/description/mimeType/size, and optional annotations (audience,
priority 0.0–1.0, lastModified). The spec calls out standard URI schemes including https://,
file://, and git://, while allowing custom schemes per RFC 3986. A missing resource returns
JSON-RPC error code -32002 ("Resource not found").
Prompts: user-invoked templates
A mcp-prompt is a reusable, parameterized message template. Discovery is
prompts/list; retrieval-with-arguments is prompts/get. A prompt definition has name,
optional title/description, and arguments (each with name, description, required). A
prompts/get result returns a description and a messages array, where each message has a
role (user or assistant) and a content block (text, image, audio, or embedded resource).
{
"jsonrpc": "2.0",
"id": 2,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": { "code": "def hello():\n print('world')" }
}
}
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"description": "Code review prompt",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review this Python code:\ndef hello():\n print('world')"
}
}
]
}
}
Because prompts are user-controlled, hosts typically surface them as explicit affordances
(slash commands), not as something the model triggers on its own. As with tools and resources,
notifications/prompts/list_changed signals list changes when listChanged was declared.
Transports: stdio and Streamable HTTP
MCP is transport-agnostic but defines two standard transports. Clients SHOULD support stdio-transport whenever possible.
stdio
In the stdio transport the client launches the server as a subprocess. The server reads
JSON-RPC messages from stdin and writes them to stdout; messages are delimited by newlines
and MUST NOT contain embedded newlines. The server MUST NOT write anything to stdout that
is not a valid MCP message, but it MAY write free-form UTF-8 logs to stderr. This makes
stdio ideal for local, single-user servers (a filesystem or Git server bundled with the app)
with no network surface at all.
Streamable HTTP
streamable-http is the current remote transport; it replaces the older
HTTP+SSE transport from revision 2024-11-05. The server exposes a single MCP endpoint path
(e.g. https://example.com/mcp) that supports both POST and GET.
- Every client→server JSON-RPC message is a new HTTP POST to that endpoint. The client
MUST send an
Acceptheader listing bothapplication/jsonandtext/event-stream. - If the POST body is a response or notification, the server returns
202 Acceptedwith no body. - If the POST body is a request, the server replies either with
Content-Type: application/json(one JSON object) or withContent-Type: text/event-stream, opening an SSE stream. The client MUST support both. On an SSE stream the server may send interim requests/notifications before the final response, then SHOULD close the stream after sending it. - The client MAY also issue an HTTP GET (with
Accept: text/event-stream) to open an SSE stream for unsolicited server→client messages; the server returnstext/event-streamor405 Method Not Allowedif it offers none.
Sessions: a server MAY assign a session id by returning an Mcp-Session-Id header on
the initialize response; if it does, the client MUST include Mcp-Session-Id on every
subsequent request. A server that has expired a session responds 404 Not Found, prompting the
client to re-initialize; a client can end a session with an HTTP DELETE carrying the header.
For resumability, the server may attach SSE event ids and honor the Last-Event-ID header on a
reconnecting GET to replay missed messages on that same stream only.
Protocol version header: once negotiated, the client MUST send MCP-Protocol-Version: <version> on all subsequent HTTP requests; if absent and otherwise unknown, the server SHOULD
assume 2025-03-26, and an unsupported value gets a 400 Bad Request.
sequenceDiagram
participant Client
participant Server
Note over Client,Server: initialization
Client->>Server: POST initialize
Server-->>Client: InitializeResult + Mcp-Session-Id: 1868a90c...
Client->>Server: POST notifications/initialized (Mcp-Session-Id)
Server-->>Client: 202 Accepted
Note over Client,Server: a request can resolve two ways
Client->>Server: POST tools/call (Mcp-Session-Id)
alt single JSON response
Server-->>Client: 200 application/json (result)
else server opens an SSE stream
Server-)Client: text/event-stream: interim messages
Server-)Client: SSE event: final result
end
Note over Client,Server: optional server-initiated channel
Client->>Server: GET (Accept: text/event-stream)
Server-)Client: SSE: server requests / notifications
Backward compatibility with the deprecated HTTP+SSE transport (2024-11-05) is achievable: a
server can host both the old SSE/POST endpoints and the new MCP endpoint, and a client probes by
POSTing an initialize request — success means the new transport; a 4xx means fall back to issuing
a GET and waiting for the legacy endpoint SSE event.
Security considerations
MCP grants "arbitrary data access and code execution paths," so the spec devotes a top-level section to trust and safety. The protocol cannot enforce these at the wire level, so they fall on implementors:
- User consent and control. Users must explicitly consent to and understand all data access and operations, and retain control over what is shared and done.
- Data privacy. Hosts must obtain explicit consent before exposing user data to servers and must not transmit resource data elsewhere without consent.
- Tool safety. Tools are arbitrary code execution; tool annotations are untrusted unless from a trusted server, and hosts must get explicit user consent before invoking any tool. The spec recommends a human in the loop able to deny any invocation.
- Sampling controls. Users must approve sampling requests and control whether sampling happens, the prompt sent, and what results the server can see — the protocol deliberately limits server visibility into prompts.
The Streamable HTTP transport adds network-specific mandates: servers MUST validate the
Origin header to prevent DNS-rebinding attacks, SHOULD bind only to 127.0.0.1 when
running locally, and SHOULD authenticate connections. MCP also defines an OAuth-based
authorization framework for HTTP transports (HTTP servers SHOULD conform; stdio servers
SHOULD instead take credentials from the environment); the 2025-06-18 revision classifies MCP
servers as OAuth Resource Servers that publish protected-resource metadata.
Putting it together
A practitioner's mental model: the host owns the model and the conversation; each client
owns one isolated session to one server; the session opens with an initialize handshake that
fixes the protocol version and negotiates capabilities; thereafter the model discovers tools with
tools/list and calls them with tools/call, reads context via resources/read, and invokes
templates via prompts/get — all as JSON-RPC messages over stdio (local) or Streamable HTTP
(remote). For how this single-agent integration model differs from cross-agent delegation, and
where the boundaries between standards fall, continue to A2A and the
protocol comparison.