Model Context Protocol (MCP)
MCP gives an AI host one protocol for discovering and invoking tools, resources, and prompts. The 2026-07-28 revision makes that protocol stateless: capability and version context travels with every request, not in a connection-bound handshake.
Lesson 14 of 17Build~75 minutesPython
Before this: Lesson 9 of Phase 11 (Function Calling), lesson 3 of Phase 11 (Structured Outputs)
On this page
Learning Objectives
- Distinguish an MCP host, client, server, transport, and server primitive.
- Build a JSON-RPC request with the metadata required by MCP 2026-07-28.
- Use
server/discoverto inspect versions, identity, and capabilities. - Return typed and cache-aware results from tools, resources, and prompts.
- Explain how modern stateless MCP interoperates with handshake-era servers.
- Choose safe state, transport, and approval boundaries for a server.
The Problem
Your application needs a database query, a calendar operation, and a file reader. Without a shared protocol, every AI host needs custom discovery, invocation, errors, transport, and authorization glue for those same capabilities.
MCP reduces that integration matrix. A server publishes a standard JSON-RPC surface. A compliant client can discover the surface, present it to a model or user, invoke it, and interpret the result without a server-specific adapter.
The important boundary is easy to miss. MCP standardizes communication. It does not decide which tool the model should call, make untrusted content safe, or turn a stateless request into durable application state. Your host and server still own those decisions.
The Concept
The three server primitives
- Tools are callable actions. Each tool has a name, description, JSON Schema input, and handler.
- Resources are named, URI-addressed content that a client can read.
- Prompts are reusable templates that a host can expose to a user.
The host is the AI application. An MCP client inside that host speaks to one server. The transport carries JSON-RPC messages between them.
Stateless requests replace the handshake
MCP 2026-07-28 removes initialize and notifications/initialized. It also removes protocol-level sessions. Every request carries the context needed to interpret it in params._meta:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "lesson-client",
"version": "1.0.0"
}
}
}
}
The protocol version and client capabilities are required. Client identity is recommended. A missing _meta, a missing required field, or a required field with the wrong type is malformed and returns Invalid Params (-32602). A well-formed version string that the server does not support returns UnsupportedProtocolVersionError (-32022). A server can process a valid request without recovering a prior negotiation record.
Stateless does not mean an application can never maintain state. It means that state is not hidden behind an MCP connection or Mcp-Session-Id. If a workflow needs continuity, the server mints an opaque handle and the client passes that handle as an ordinary tool argument on later calls. Authorization must still be checked on every request.
Discovery and version selection
Every modern server implements server/discover. The result advertises supported versions, capabilities, and server identity:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"ttlMs": 3600000,
"cacheScope": "public",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "demo-server",
"version": "1.0.0"
}
}
}
}
A client may call another method directly and handle a version error, but discovery makes capability display and version selection explicit. An unsupported version returns UnsupportedProtocolVersionError with code -32022. Its data contains supported, an array of server revisions, and requested, the rejected revision.
On stdio, a dual-era client probes with server/discover. A discovery result or a recognized modern error such as UnsupportedProtocolVersionError identifies a modern server. Any error or timeout that is not recognized as modern permits fallback to the 2025-11-25 initialize flow. Legacy behavior is compatibility code, not the modern default.
Results are explicit
Every core 2026-07-28 result has resultType:
completemeans the operation finished.input_requiredmeans the server needs another round trip through the Multi Round-Trip Requests pattern. Core servers may return it only fromtools/call,resources/read, orprompts/get.
Clients must treat a legacy result that omits resultType as complete.
Servers should include io.modelcontextprotocol/serverInfo in every result's _meta. This identity is self-reported and is for display, logging, and debugging, not for security decisions.
List and read results also carry ttlMs and cacheScope. A deterministic tools/list order plus a freshness hint lets clients cache discovery safely and improves prompt-cache stability. cacheScope: public permits shared caching; private confines reuse to the calling context.
The wire format and transport
MCP uses JSON-RPC 2.0 over stdio or Streamable HTTP.
- A request has
jsonrpc,id,method, andparams. - A response has the matching
idand eitherresultorerror. - A notification has no
idand expects no response.
Modern Streamable HTTP exposes one endpoint that accepts POST. Each JSON-RPC message gets its own POST. A request POST receives either one JSON object or a request-scoped Server-Sent Events stream that ends with the final response. An accepted notification POST receives HTTP 202 with no response body; this core revision defines no client-to-server notifications over Streamable HTTP.
There is no standalone MCP GET stream, DELETE session endpoint, Mcp-Session-Id, or Last-Event-ID replay in 2026-07-28. Long-lived change notifications use a subscriptions/listen POST whose response remains open as an SSE stream.
Client input without server-initiated requests
Older revisions let a server send requests such as sampling/createMessage, roots/list, or elicitation/create over a stream. The current protocol uses Multi Round-Trip Requests instead. An eligible tool call, resource read, or prompt get returns resultType: input_required with at least one of inputRequests or requestState. The client gathers any requested input, retries the original method with a new JSON-RPC ID and the corresponding inputResponses, and echoes the exact requestState when one was provided. If no inputRequests were present, the retry omits inputResponses.
Roots, Sampling, and Logging remain functional but are deprecated, so new implementations should not adopt them. Existing Roots or Sampling requests travel inside MRTR inputRequests, never as independent server-to-client JSON-RPC requests. Prefer explicit file or directory parameters, resource URIs, server configuration, and direct model-provider integration. Use stderr for stdio diagnostics and OpenTelemetry for production telemetry.
mcp-nxm-collapse
Build It
Step 1: register a server surface
Registration stays simple even though the request contract changed:
server = MCPServer("demo-server")
@server.tool(
"add",
"Add two integers.",
{
"type": "object",
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
}
)
def add(a: int, b: int) -> dict:
return {"sum": a + b}
The shipped implementation in code/main.py also registers a resource and prompt. It deliberately uses the standard library so you can see each envelope rather than delegating the protocol to an SDK.
Step 2: attach metadata to every request
def request(method, params=None):
body_params = dict(params or {})
body_params["_meta"] = {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "demo-client",
"version": "1.0.0"
}
}
return {
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": body_params
}
Do not cache this metadata only in a connection object. The server validates it on each request.
Step 3: optionally discover before listing
Call server/discover, choose a supported version, then call tools/list. A direct tools/list is also valid if you already know the version and can handle -32022.
The demo returns tool lists in name order and attaches ttlMs, cacheScope, resultType, and server identity. A tool call returns a complete, non-cacheable result because its output can depend on current state.
Step 4: map the same request to HTTP
A remote tools/call POST includes headers that mirror the JSON-RPC 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: add
The MCP-Protocol-Version header must match the version in _meta. Mcp-Method is required on every JSON-RPC request and must match method. Mcp-Name is required only for tools/call, resources/read, and prompts/get, where it must match the tool name, resource URI, or prompt name. A missing required header or mismatch returns HTTP 400 with HeaderMismatch code -32020.
Step 5: enforce safety outside protocol state
- Validate authorization and audience on every HTTP request.
- Bind local servers to localhost and validate
Originon Streamable HTTP. - Mark mutating tools with
destructiveHint: trueand require host approval. - Pass directory and file scope explicitly instead of depending on deprecated Roots.
- Treat resources and tool output as untrusted data.
- Keep stdout reserved for JSON-RPC under stdio; write diagnostics to stderr.
Use It
Run the lesson from its directory:
python3 code/main.py
cd code
python3 -m unittest discover tests -v
The first line should report discovery of demo-server at protocol 2026-07-28. Then inspect MCPClient.request: it reconstructs _meta for every call. Remove the metadata from one request and observe the server reject it.
Ship It
outputs/skill-mcp-server-designer.md turns a domain into a stateless MCP design. Its acceptance gate requires a discovery result, per-request metadata policy, deterministic cache-aware lists, explicit state handles, transport headers, authorization, and approval rules.
Continue the MCP Deep Dive
This lesson gives you the protocol model. Phase 13 turns four production boundaries into separate build-and-verify lessons:
- MCP Tool Contracts and Content covers closed input schemas, structured content, routing metadata, opaque pagination, completion authorization, and the difference between protocol and tool-domain errors.
- MCP Reliability, Cancellation, and Flow Control covers request cancellation, durable task cancellation, deadlines, idempotency, backpressure, proxy buffering, and reconnect behavior.
- MCP Registry Supply Chain, Admission, Drift, and Rollback covers namespace proof, artifact provenance, immutable pins, live drift, Registry status, admission evidence, and rollback.
- MCP Conformance Engineering covers golden and negative wire transcripts, strict version eras, SDK differentials, proxy evidence, redaction, health gates, and release rollback.
Follow them in order when the server will cross a team or trust boundary. Together they move from “the method works” to “the contract remains safe and diagnosable through deployment.”
Exercises
- Add a
subtracttool and confirmtools/listremains alphabetically ordered. - Remove the protocol-version key and verify Invalid Params (
-32602). Then send the well-formed but unsupported version2025-11-25, verify-32022, confirmrequestedechoes that revision, and choose fromsupported. - Add a server-minted
draftIdto a create operation, then require it as an argument to update. Explain why that is application state rather than a protocol session. - Return
input_requiredfrom a tool that needs user confirmation. Retry the original call with a new ID, aninputResponsesentry, and the exactrequestStateinstead of inventing a server-to-client JSON-RPC request. - Sketch a dual-era stdio client. Treat a result or recognized modern error as modern, and permit fallback to
initializeonly for an unrecognized error or timeout.
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| MCP | "Tool protocol for LLMs" | JSON-RPC protocol for server discovery, tools, resources, prompts, and extensions |
| Host | "The AI app" | Owns the model and UI and mounts one or more MCP clients |
| Client | "The connector" | Speaks MCP to one server on behalf of a host |
| Stateless MCP | "No session" | Every request carries version and capabilities; no protocol state is keyed by a connection |
server/discover |
"Capability probe" | Required server method advertising versions, capabilities, and identity |
resultType |
"Result state" | Marks a result as complete or input_required |
| State handle | "Workflow id" | Server-minted application identifier passed as an ordinary argument |
| Streamable HTTP | "Remote transport" | One POST endpoint with JSON or request-scoped SSE responses |
| MRTR | "Ask and retry" | Input request embedded in a result, followed by a retry of the original operation |
Further Reading
Check yourself
Quick quiz
In MCP 2026-07-28, where does a client send its protocol version and capabilities?
- Only in the HTTP cookie
- Only in an initialize request
- In params._meta on every request
- In a server-side session record
Show the answer
C. In params._meta on every request Modern MCP is stateless. Each request carries the protocol version and client capabilities in params._meta.
What must every modern MCP server implement for up-front version and capability discovery?
- server/discover
- sessions/create
- logging/setLevel
- initialize
Show the answer
A. server/discover server/discover advertises supported versions, server capabilities, and server identity.
Which Streamable HTTP shape is current in MCP 2026-07-28?
- A DELETE request after every tool call
- A WebSocket session identified by Mcp-Session-Id
- One POST per JSON-RPC message; request POSTs receive JSON or request-scoped SSE
- A GET stream plus a separate POST endpoint
Show the answer
C. One POST per JSON-RPC message; request POSTs receive JSON or request-scoped SSE The current transport has one POST endpoint. Request POSTs receive JSON or request-scoped SSE; an accepted notification POST receives HTTP 202 with no body.
How should a server distinguish missing protocol-version metadata from a well-formed but unsupported version?
- Return -32022 for both cases
- Return HTTP 301 for a missing version and mint Mcp-Session-Id for an unsupported version
- Return -32602 for missing or non-string metadata and -32022 with supported/requested for an unsupported string
- Accept a missing version and return -32601 for an unsupported version
Show the answer
C. Return -32602 for missing or non-string metadata and -32022 with supported/requested for an unsupported string A missing or ill-typed required field is malformed JSON-RPC params. UnsupportedProtocolVersionError applies only when a supplied version string is not implemented.
A multi-step tool needs continuity across calls. What is the modern MCP design?
- Put mutable state in clientInfo
- Restore Mcp-Session-Id for this tool only
- Mint an opaque handle and require it as a later tool argument
- Store state behind the TCP connection
Show the answer
C. Mint an opaque handle and require it as a later tool argument Cross-call state is explicit application state. A server-minted handle travels as an ordinary, authorized argument.
How does a modern server ask for user, model, or root input during a tool operation?
- It writes the question to stderr
- It opens a second SSE connection
- It returns input_required and the client retries with inputResponses
- It sends an independent server-to-client JSON-RPC request
Show the answer
C. It returns input_required and the client retries with inputResponses The Multi Round-Trip Requests pattern embeds inputRequests in an input_required result and retries the original operation.
Questions people ask
What is MCP in AI and why does it matter?
MCP is a protocol that defines how an AI host discovers and calls tools, reads resources and uses saved prompts from a server. It matters because one standard replaces many one-off integrations, so the same connector works across different assistants and apps.
What is the difference between an MCP server and an API?
An API is any interface a service exposes. An MCP server wraps tools or data in a standard format that AI hosts know how to list and call, with names, descriptions and input schemas the model can read. Many MCP servers sit on top of an existing API.
Lessons from AI Engineering from Scratch by Rohit Ghumare and contributors, MIT license. Notes for owners by Lozo. License