Model Context Protocol Series — Presentation 01

The Model Context Protocol — A Visual Tour

What MCP actually is on the wire — JSON-RPC primitives, host/client/server topology, capability negotiation, lifecycle, and the version timeline from 2024-11-05 to 2026-07-28, the revision that made MCP stateless. Both eras are covered; handshake-era material is tagged “before 2026-07-28”.

MCP JSON-RPC 2.0 Tools Resources Prompts Sampling Elicitation
Host → Client → Server → Initialize → Capabilities → Primitives → Shutdown
00

Topics We'll Cover

01

The M×N Problem — Why MCP Exists

By late 2024 every serious LLM application had reinvented the same wheel: a bespoke way to plug a model into a database, a filesystem, a ticketing system, an internal API. M AI applications times N data sources is M×N integrations — each one a brittle bag of function-calling JSON, prompt scaffolding, and authentication glue.

Anthropic open-sourced the Model Context Protocol in November 2024 to collapse this to M+N: each application speaks one protocol, each tool ships one server. The marketing line is “USB-C for AI” — a single shaped plug that any compliant host can talk to.

Before MCP — M×N integrations

Every host wrote its own connector for every tool. Cursor + Postgres, Claude Desktop + Postgres, Continue + Postgres, … same job, three implementations, three sets of bugs.

With MCP — M+N integrations

The Postgres team writes one server. Every compliant host gets it for free. The work moves from N-app-teams to one tool-team.

Why it stuck

MCP is small enough to read in an afternoon, JSON-RPC over a stream that any language can speak, and it ships with reference SDKs in Python, TypeScript, Java, C#, Kotlin, Swift, Go and Ruby. The barrier to writing a server is “define a function and a JSON Schema”.

02

Host / Client / Server — The Three Roles

The MCP architecture has three named participants. Mixing them up is the single most common source of confusion when reading the spec, so it's worth pinning down up front:

RoleWho plays itJob
HostClaude Desktop, Claude Code, Cursor, Zed, Continue, VS Code, your own agent runtimeOwns the LLM. Orchestrates the conversation. Decides which servers to load.
ClientA library inside the host — one client instance per connected serverSpeaks the JSON-RPC protocol. 1:1 with a server. Stateful.
ServerThe Postgres server, the GitHub server, your own internal data serverExposes tools, resources, prompts. Knows nothing about the LLM. Pure protocol citizen.
Process boundaries — one host, many clients, many servers Host (e.g. Claude Desktop) owns the LLM · runs the agent loop Client A Client B Client C one client instance per server Server A — Postgres stdio child process Server B — GitHub Streamable HTTP, OAuth Server C — Filesystem stdio child process
Mental model

Think of the host as the operating-system shell, the clients as file descriptors, and the servers as long-running daemons. The host is where trust lives; the servers are dumb actuators that the host has chosen to load.

03

JSON-RPC 2.0 — The Wire Alphabet

MCP doesn't invent a new wire format. Every message is a JSON-RPC 2.0 envelope, defined by the 5-page spec from 2010. There are exactly three message shapes:

Request

Has an id. Expects a response. {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}

Response

Carries the same id. Either result or error, never both. {"jsonrpc":"2.0","id":1,"result":{...}}

Notification

Has no id. Fire-and-forget. {"jsonrpc":"2.0","method":"notifications/progress"} (params omitted; the deck's earlier example, notifications/initialized, exists only before 2026-07-28).

The Standard Error Codes

JSON-RPC defines reserved codes; MCP layers a small set on top. Anything outside -32768..-32000 is yours to define for tool-level failures.

CodeNameWhen
-32700Parse errorThe JSON itself was malformed
-32600Invalid requestNot a valid JSON-RPC envelope
-32601Method not foundThe method name is unknown
-32602Invalid paramsThe params didn't match the schema
-32603Internal errorCatch-all for server bugs
-32002Resource not found before 2026-07-28MCP-specific (resources/read on a missing URI). In 2026-07-28 a missing resource is -32602; clients should still accept -32002 from older servers.
-32020HeaderMismatch 2026-07-28An HTTP header (Mcp-Method, Mcp-Name…) disagrees with the JSON body
-32021MissingRequiredClientCapability 2026-07-28The request needs a client capability it did not declare in _meta
-32022UnsupportedProtocolVersion 2026-07-28The server does not speak the version the request declared; data.supported lists the ones it does
Important: tool errors are NOT JSON-RPC errors

If a tool runs and the operation fails (e.g. create_issue hit a 401), the JSON-RPC call still succeeds with a result. The tool result carries isError: true and the failure message. JSON-RPC errors are reserved for protocol-level problems — bad params, missing methods, unmarshalable JSON. Mixing the two is the bug every new server author writes once.

Source: MCP specification 2026-07-28 — Overview: error codes; Key changes (accessed 2026-10-08).

04

The Five Primitives

The protocol's surface area is small: three primitives a server can offer, two that a client can offer back. Everything else in MCP is plumbing for these.

Server-side primitives (server → client)

Tools

Side-effecting functions the model can call. tools/list, tools/call. Each tool ships a JSON Schema for its arguments and (since 2025-06-18) for its structured output. Annotations declare read-only / destructive / open-world.

Resources

Read-only data identified by URI. resources/list, resources/read, resources/subscribe. Subscriptions push resources/updated notifications — how a server tells the client “the file you were watching changed”.

Prompts

Reusable, parameterised templates the user can invoke explicitly — typically as slash commands or quick actions. prompts/list, prompts/get. Different from a tool: a prompt is composed and shown to the user, a tool is executed.

Client-side primitives (client → server)

Sampling deprecated in 2026-07-28

sampling/createMessage. The server asks the client to call its LLM. Lets a server build agentic behaviour without bringing its own model. The host MUST gate this with explicit user approval.

Elicitation

elicitation/create, standardised in 2025-06-18. The server presents a typed, schema-constrained question (“destination zip code?”) and the client renders a form. Returns accept, decline, or cancel.

Two more first-class concepts

Roots are URIs the host has authorised the server to touch (e.g. a project directory). roots/list is a client-side request.

Logging is the notifications/message stream and logging/setLevel request — the server can emit structured log records to the client at standard syslog levels.

What 2026-07-28 changed here 2026-07-28

No more server-initiated requests. Sampling, elicitation and roots/list still exist, but a server now asks for them by returning a result with resultType: "input_required"; the client gathers the answers and retries the original tools/call, prompts/get or resources/read with inputResponses (multi round-trip requests, deck 03).

Deprecated: Roots, Sampling and Logging (still functional, earliest removal in the first revision on or after 2027-07-28). logging/setLevel is gone; a client sets io.modelcontextprotocol/logLevel per request. resources/subscribe is replaced by subscriptions/listen.

Source: MCP specification 2026-07-28 — Multi round-trip requests; Deprecated features; Subscriptions (accessed 2026-10-08).

05

Capability Negotiation

before 2026-07-28 The handshake form is shown first; the callout at the end gives the 2026-07-28 form.

MCP explicitly avoids feature-detection by trial-and-error. At handshake time both sides exchange a capabilities object; nothing else can be assumed. This is what lets a Python server written against the 2024-11-05 spec keep working with a Claude Desktop running 2025-06-18.

Server → client (initialize result, abridged)
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools":     { "listChanged": true },
      "resources": { "subscribe":   true,
                       "listChanged": true },
      "prompts":   { "listChanged": true },
      "logging":   {},
      "completions": {}                // argument autocompletion
    },
    "serverInfo": { "name": "my-server", "version": "0.4.1" },
    "instructions": "Use the ‘query’ tool to read the database."
  }
}
Read this carefully

An empty object ("logging": {}) means “I support this primitive but with no sub-options”. An absent key means “I do not support this primitive”. The client MUST NOT call logging/setLevel on a server that didn't declare logging. Calling an undeclared primitive returns -32601 Method not found.

Sub-flags worth knowing

FlagMeaning
tools.listChangedServer may emit notifications/tools/list_changed when its tool catalogue changes at runtime
resources.subscribeServer supports resources/subscribe — per-URI change watching
resources.listChangedServer may emit a global resources-changed notification (catalogue-level)
sampling (client side)Client has an LLM available and will service sampling/createMessage
elicitation (client side)Client can render schema-constrained user prompts
roots.listChangedClient will tell the server when the root list changes (e.g. user opened a new project) — the notification is removed in 2026-07-28
Capabilities in 2026-07-28 2026-07-28

There is no initialize, so nothing is negotiated once. Every request carries io.modelcontextprotocol/clientCapabilities (and its protocol version) in _meta; a server that needs an undeclared client capability returns -32021 MissingRequiredClientCapability. The server advertises its own capabilities through server/discover, which every server MUST implement and a client MAY call first. “Absent ≠ empty” still holds.

Source: MCP specification 2026-07-28 — Overview: _meta; Discovery (accessed 2026-10-08).

06

Lifecycle — initialize → operate → shutdown before 2026-07-28

Up to and including 2025-11-25 (the “legacy” era in the spec's own terms), every MCP session goes through the same three phases. The protocol is strict about ordering: a request issued in the wrong phase is a protocol error.

Client Server Initialization initialize { protocolVersion, capabilities, clientInfo } result { protocolVersion, capabilities, serverInfo } notifications/initialized (no id) Operation tools/list { tools: [...] } tools/call { name, arguments } notifications/progress (any time) { content: [...], structuredContent, isError } primitive calls in any order, both directions, until close Shutdown close transport → both sides drop state. No "shutdown" RPC; ends with the connection.
Protocol-version negotiation

The client proposes a version. The server picks one it supports (typically the highest mutually-supported value) and returns it in the initialize result. From 2025-06-18 onward the agreed version MUST also be sent on every subsequent HTTP request as MCP-Protocol-Version: 2025-06-18 — this is what catches misconfigured proxies and stale clients.

06b

Lifecycle in 2026-07-28 — No Handshake, Every Request Stands Alone 2026-07-28

Revision 2026-07-28 removed initialize, notifications/initialized, ping and protocol-level sessions. MCP is now stateless: every request carries its protocol version and client capabilities in _meta, and a server must not infer anything from earlier requests on the same connection.

Client Server Discover (optional) server/discover { _meta } { supportedVersions, capabilities, serverInfo } Requests tools/call { name, arguments, _meta: { protocolVersion, clientCapabilities, clientInfo } } { resultType: "complete", content, ... } tools/list { _meta: { protocolVersion: "1900-01-01" } } error -32022 { supported: [...], requested } client picks a version from "supported" and retries; each request is judged on its own Stop no session to tear down: a connection or stdio process is not a conversation

Version negotiation without initialize

The client declares a version on every request (_meta, plus the MCP-Protocol-Version header on HTTP). A server that does not implement it answers UnsupportedProtocolVersionError (-32022) listing supported; the client retries with a common version. server/discover lets it learn the list up front, and doubles as a probe for legacy servers on stdio.

What replaced the session

State that must span calls is an explicit handle the server mints and the client passes back as an ordinary argument. Long-lived streams are requests too: subscriptions/listen opens one response stream of opted-in change notifications (deck 02). A server that needs more input mid-call returns resultType: "input_required" (deck 03).

Two eras on the wire today

The spec calls handshake-based revisions (2025-11-25 and earlier) legacy and 2026-07-28 onwards modern; a dual-era implementation speaks both. The official Python SDK (mcp 2.x) is dual-era. Chapter 2 of Agent Protocols Explained animates both life cycles side by side from a simulator checked against that SDK.

Source: MCP specification 2026-07-28 — Key changes; Versioning and compatibility; Overview: statelessness; Discovery (accessed 2026-10-08).

07

Versioning — The Spec Timeline

MCP versions are date-stamped, not semver. The string is literally the date the spec was frozen. Five revisions have shipped; as of October 2026 the current one is 2026-07-28:

VersionHeadlineWhat changed
2024-11-05Initial releaseOpen-sourced by Anthropic. JSON-RPC + stdio + HTTP/SSE. Tools, resources, prompts, sampling, roots, logging.
2025-03-26Streamable HTTP & authNew Streamable HTTP transport (HTTP+SSE deprecated). OAuth 2.1 normative. Audio content type. Tool annotations (read-only/destructive/etc).
2025-06-18Handshake era before 2026-07-28
(this deck's original “current production target”)
JSON-RPC batching removed. Elicitation standardised. Structured tool output (separate structuredContent). Resource links in tool results. Mandatory MCP-Protocol-Version header. OAuth resource server clarifications (RFC 8707 audience binding).
2025-11-25Last handshake revision before 2026-07-28OpenID Connect discovery for auth servers. Client ID Metadata Documents recommended for registration. URL-mode elicitation. Tool calling inside sampling. Icons. Experimental tasks. Incremental scope consent.
2026-07-28Current — stateless 2026-07-28initialize/initialized, sessions (Mcp-Session-Id), ping and Last-Event-ID resumption removed. Version + capabilities in every request's _meta; server/discover. Server→client requests replaced by resultType: "input_required" + retry. subscriptions/listen. Tasks moved to an extension. Roots, Sampling, Logging and Dynamic Client Registration deprecated.

Why dates, not semver?

Date-stamping makes the spec trivially comparable (string sort ≡ chronological order) and dodges the semver fight over what counts as “breaking”. The compatibility contract is: servers SHOULD support a window of recent versions; clients negotiate the highest both sides know — through initialize before 2026-07-28, and per request (retry on -32022, or server/discover first) from 2026-07-28.

Migration footguns to remember

Batched JSON-RPC died. If you have a 2024-11-05 server happily returning [{...}, {...}], a 2025-06-18 client will refuse it. The reasoning was that batching never composed cleanly with SSE streams and almost no SDKs implemented it correctly.

HTTP+SSE is deprecated, not removed. Existing servers keep working; new ones SHOULD use Streamable HTTP. Deck 02 covers the wire-level differences.

2026-07-28 is the biggest break since launch. A modern client talking to a legacy server must fall back to initialize; a modern server must not expect it. Features are now retired through a published lifecycle: Deprecated features keep working for at least twelve months.

Source: MCP specification 2026-07-28 — Key changes; Versioning and compatibility; Deprecated features; 2025-11-25 key changes (accessed 2026-10-08).

08

Worked Example — A Full Annotated Exchange before 2026-07-28

Protocol version 2025-06-18. End-to-end: a Claude Desktop client connects to a tiny weather server, lists tools, calls one, receives a structured result. Every line on the wire is shown.

1. initialize (client → server, request)
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
  "protocolVersion":"2025-06-18",
  "capabilities":{
    "sampling":{},
    "elicitation":{},
    "roots":{"listChanged":true}
  },
  "clientInfo":{"name":"claude-desktop","version":"0.13.0"}
}}
2. initialize result (server → client, response)
{"jsonrpc":"2.0","id":1,"result":{
  "protocolVersion":"2025-06-18",
  "capabilities":{
    "tools":{"listChanged":true},
    "resources":{"subscribe":false,"listChanged":true}
  },
  "serverInfo":{"name":"weather-server","version":"1.2.0"},
  "instructions":"Provide forecasts via the ‘forecast’ tool. Locations must be ISO 3166-2 codes."
}}
3. notifications/initialized (client → server, notification — no id)
{"jsonrpc":"2.0","method":"notifications/initialized"}
4. tools/list (request / response)
// → client
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
// ← server
{"jsonrpc":"2.0","id":2,"result":{
  "tools":[{
    "name":"forecast",
    "title":"Weather Forecast",
    "description":"Return a 24-hour forecast for an ISO 3166-2 region.",
    "inputSchema":{
      "type":"object",
      "properties":{ "region":{"type":"string","pattern":"^[A-Z]{2}-[A-Z0-9]{1,3}$"} },
      "required":["region"]
    },
    "outputSchema":{
      "type":"object",
      "properties":{
        "tempC":{"type":"number"},
        "windKph":{"type":"number"},
        "summary":{"type":"string"}
      },
      "required":["tempC","windKph","summary"]
    },
    "annotations":{ "readOnlyHint":true, "openWorldHint":false }
  }]
}}
5. tools/call (request / response, with structured output)
// → client
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
  "name":"forecast",
  "arguments":{"region":"GB-LND"}
}}
// ← server
{"jsonrpc":"2.0","id":3,"result":{
  "content":[{"type":"text","text":"London: 12°C, 14 km/h, light rain easing this evening."}],
  "structuredContent":{"tempC":12.0,"windKph":14.0,"summary":"light rain easing"},
  "isError":false
}}
What ‘structured output’ gives you

The model still sees the human-readable content — that's what gets concatenated into the LLM's context. The structuredContent is for downstream code: a workflow engine that wants to branch on tempC doesn't have to regex it out of the prose. Validation against outputSchema is the host's responsibility — but if the schema is published, both sides can rely on it.

08b

Worked Example in 2026-07-28 — The Same Call, Stateless 2026-07-28

The same weather tool under 2026-07-28. There is no initialize and no notifications/initialized: the first message can be the tool call itself. Steps 1–3 of the previous slide disappear; what is left is longer per message and shorter overall.

1. tools/list (request / response; abridged)
// → client
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{
  "_meta":{
    "io.modelcontextprotocol/protocolVersion":"2026-07-28",
    "io.modelcontextprotocol/clientInfo":{"name":"example-host","version":"1.0.0"},
    "io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}
  }
}}
// ← server
{"jsonrpc":"2.0","id":1,"result":{
  "resultType":"complete",
  "tools":[{ "name":"forecast", ... }],
  "ttlMs":300000,
  "cacheScope":"public",
  "_meta":{"io.modelcontextprotocol/serverInfo":{"name":"weather-server","version":"1.2.0"}}
}}
2. tools/call over Streamable HTTP (headers + body)
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: forecast

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
  "name":"forecast",
  "arguments":{"region":"GB-LND"},
  "_meta":{
    "io.modelcontextprotocol/protocolVersion":"2026-07-28",
    "io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}
  }
}}
// ← server
{"jsonrpc":"2.0","id":2,"result":{
  "resultType":"complete",
  "content":[{"type":"text","text":"London: 12°C, 14 km/h, light rain easing this evening."}],
  "structuredContent":{"tempC":12.0,"windKph":14.0,"summary":"light rain easing"},
  "isError":false
}}
What changed, line by line

_meta on every request replaces the handshake (version and capabilities are required; clientInfo is a SHOULD). resultType is required on every result ("complete" here; a client treats a legacy result without it as complete). List results carry ttlMs/cacheScope caching hints. On HTTP, Mcp-Method/Mcp-Name headers mirror the body so gateways can route without parsing JSON, and there is no Mcp-Session-Id. The ttlMs value here is illustrative.

Source: MCP specification 2026-07-28 — Overview: _meta and resultType; Tools; Caching; Streamable HTTP (accessed 2026-10-08).

09

What to Take Away

Where to next

Deck 02 dives into the transports — stdio, the deprecated HTTP+SSE, and the modern Streamable HTTP. Deck 03 walks through writing a real server in Python and TypeScript. Deck 04 is the one your security team will want to read.

For animated, simulator-driven versions of these slides in both eras, see Agent Protocols Explained (chapters 1–3).