The Model Context Protocol, in depth
You have now built a tool framework. It works, it validates, it fails gracefully. And it is entirely yours — the tools live in your process, in your language, behind your decorator.
That is fine until the day someone else has a tool you want.
Why a protocol exists at all
State the problem as arithmetic.
You have \(N\) AI applications: your customer support agent, your internal research assistant, a coding agent, someone’s Slack bot. You have \(M\) systems worth connecting to: Jira, Postgres, GitHub, Google Drive, your internal orders service, a vector store.
Without a standard, connecting them means writing a bespoke adapter for each pair. That is \(N \times M\) integrations. Ten applications and twenty systems is two hundred adapters, each written by someone with partial knowledge of both ends, each needing its own maintenance when either end changes.
Worse, none of that work is reusable. The team that wrote the Jira adapter for the support agent wrote it against the support agent’s tool interface. The coding agent team writes it again.
With a standard protocol in the middle, each application implements the protocol once and each system implements it once. \(N \times M\) becomes \(N + M\). Thirty pieces of work instead of two hundred.
This is not a novel insight — it is exactly the argument that produced the Language Server Protocol, which is why editors no longer need a bespoke integration per programming language. The Model Context Protocol, introduced by Anthropic in November 2024 and now developed as an open specification, is the same move applied to AI applications and tools.
Saying it out loud. MCP exists because integration work was quadratic. If you have N AI applications and M systems worth connecting to, then without a standard you write an adapter per pair — ten apps and twenty systems is two hundred adapters, each maintained by someone with partial knowledge of both ends, and none of it reusable, because the Jira adapter written for the support agent was written against that agent’s interface. Put a protocol in the middle and each app implements it once and each system implements it once, so N times M becomes N plus M — thirty pieces of work instead of two hundred. It’s not a novel insight; it’s the same argument that produced the Language Server Protocol, which is why editors no longer need one integration per language.
The architecture: hosts, clients, servers
Three roles, and the naming trips everyone up at first, so be precise.
The host is the AI application. Claude Desktop, your agent, an IDE with an AI assistant, a support bot. The host owns the conversation, decides which servers to connect to, orchestrates tool use, and enforces policy — including asking the user for approval before something dangerous happens. It is where the model lives and where the trust decisions get made.
The client is a component inside the host, one per connected server. It speaks the protocol: sends requests, receives responses, translates between the protocol’s tool format and whatever your model API expects. If your host connects to four servers, it runs four clients. You will build one of these in the next chapter.
The server exposes capabilities. It is usually an adapter sitting in front of something that already exists — a database, a SaaS API, a filesystem — and its job is to advertise what it offers, execute requests, and return well-formed results. Servers can be local processes on the user’s machine or remote services over HTTP.
The critical property: the host and the server know nothing about each other’s internals. The server does not know which model is calling it. The host does not know whether the server is Python, Go, or a shell script. That decoupling is the entire value proposition.
Saying it out loud. Three roles, and the naming trips everyone up. The host is the AI application — Claude Desktop, your agent, an IDE assistant. It owns the conversation, picks which servers to connect to, and makes the trust decisions, including asking the user before something dangerous happens. The client is a component inside the host, one per connected server, and it just speaks the protocol; four servers means four clients. The server exposes capabilities, usually as an adapter in front of something that already exists. The property that makes it valuable is that the two ends know nothing about each other’s internals — the server doesn’t know which model is calling it, the host doesn’t know if the server is Python or a shell script. That decoupling is the entire value proposition.
The wire: JSON-RPC and transports
Messages are JSON-RPC 2.0 — a small, boring, language-agnostic envelope format that has been around since 2010. Boring is the right call for a protocol layer.
There are four message shapes:
- Requests — a call that expects an answer, carrying a
method,params, and anid. - Results — the successful answer, echoing the
id. - Errors — the failed answer, with a numeric
codeand amessage. - Notifications — one-way messages with no
idand no reply.
A tool call on the wire looks like this:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "check_stock",
"arguments": {"sku": "SKU-1002", "warehouse": "AMS"}
}
}
Two transports carry those messages.
stdio runs the server as a subprocess of the host and speaks over standard input and output. It is fast, has no network surface, and is the right choice for anything touching the user’s local machine — filesystem, local git repository, local database. Debugging tip you will need in the next chapter: the server’s stdout is the protocol channel, so anything a server prints to stdout corrupts the stream. Log to stderr.
Streamable HTTP is the remote transport. A single HTTP endpoint accepts POSTs; responses come back as plain JSON, or as a Server-Sent Events stream when the server wants to push progress updates during a long call. An older HTTP+SSE transport, which required two endpoints, is deprecated — do not build against it.
Saying it out loud. On the wire it’s JSON-RPC 2.0 — a small, boring envelope format from 2010, and boring is exactly right for a protocol layer. Four message shapes: requests with a method and an ID, results echoing that ID, errors with a numeric code, and one-way notifications with no ID. Two transports carry it. Stdio runs the server as a subprocess and talks over standard in and out, which is fast and has no network surface, so it’s the right pick for anything touching the user’s machine. The gotcha that will bite you the first day: stdout is the protocol channel, so anything the server prints to stdout corrupts the stream — log to stderr. The other transport is streamable HTTP for remote servers; the older two-endpoint HTTP-plus-SSE transport is deprecated, so don’t build on it.
The primitives
MCP defines six capability types. Three are offered by servers to clients, three the other way around.
Server side:
Tools are functions the model can call.
read_file, execute_sql, create_ticket.
This is the primitive that matters — near-universal client support, and the reason MCP exists in practice.
Resources are contextual data identified by a URI: a file’s contents, a database schema, a configuration blob, a log. The idea is that the host can pull them into context deliberately, rather than the model calling a tool to fetch them. Support across clients is roughly a third.
Prompts are reusable prompt templates the server offers, so a server can teach the client higher-level workflows built on its own tools. Similar support level, and a real security question attached: a prompt is a third party injecting instructions into your execution path. Treat them with suspicion, especially from servers you do not control.
Client side — and this is where the ground has moved, so read the next section before you build on any of it:
Sampling let a server ask the client to run an LLM completion on its behalf. Elicitation lets a server pause mid-operation and ask the user a question through the client’s UI. Roots let a client tell a server which filesystem boundaries it may operate within.
Client-side capability support was always thin — single-digit percentages across tracked clients. Two of the three are now formally deprecated.
Saying it out loud. MCP defines a handful of capability types, but in practice tools are the one that matters — near-universal client support, and the reason anyone adopts MCP at all. Resources are contextual data behind a URI, like a file or a schema, that the host can pull in deliberately, and support for those is much patchier. Prompts are reusable templates a server offers, and they carry a real security question, because a prompt is a third party injecting instructions into your execution path — treat them with suspicion from any server you don’t control. The client-side capabilities were always thin, single-digit adoption, and some are now deprecated, so before you build on anything beyond tools, check the current spec rather than trusting a tutorial or this chapter.
Tool definition
A tool definition is JSON with these fields:
name— unique identifier on this servertitle— optional human-readable display namedescription— the prompt, per Chapter 1inputSchema— JSON Schema for the argumentsoutputSchema— optional JSON Schema for the structured resultannotations— optional behavior hints
{
"name": "check_stock",
"title": "Check shippable stock for one SKU",
"description": "Report how many units of one SKU are available to ship today. Get the SKU from find_sku first if you only have a product name.",
"inputSchema": {
"type": "object",
"properties": {
"sku": {"type": "string", "description": "Exact SKU identifier, e.g. \"SKU-1001\"."},
"warehouse": {"enum": ["AMS", "SIN", "any"], "default": "any",
"description": "Which warehouse to check."}
},
"required": ["sku"]
},
"outputSchema": {
"type": "object",
"properties": {
"on_hand": {"type": "integer", "description": "Units physically available."},
"shippable": {"type": "boolean", "description": "False when on_hand is 0."}
},
"required": ["on_hand", "shippable"]
},
"annotations": {"readOnlyHint": true, "idempotentHint": true, "destructiveHint": false}
}
Everything from Chapter 1 applies without modification.
title, description, and outputSchema are marked optional in the specification; treat all three as required in anything you ship.
The annotations field deserves a warning.
The defined hints are readOnlyHint, idempotentHint, destructiveHint, and openWorldHint, and they are exactly what they sound like.
But they are hints, self-reported by the server, with nothing verifying them.
A malicious server can mark a tool readOnlyHint: true and then delete your data.
Use them to improve the user experience for servers you trust — skipping a confirmation dialog on a genuinely read-only call, for instance.
Never use them as a security control.
Saying it out loud. A tool definition is just a name, a description, an input schema, and optionally an output schema and some annotations — so everything about writing descriptions as prompts carries over unchanged. Title, description, and output schema are marked optional in the spec, and I’d treat all three as mandatory in anything I ship. The part I’d warn about is annotations: hints like read-only, idempotent, and destructive are self-reported by the server with nothing verifying them. A malicious server can mark a tool read-only and then delete your data. So use them to improve UX for servers you trust — skipping a confirmation dialog on a genuinely read-only call — and never as a security control.
Tool results
A result carries a content array of blocks, and optionally a structuredContent object.
Unstructured content blocks come in types: text, image and audio (base64 with a MIME type), plus resource links and embedded resources.
Structured content is a JSON object validated against the tool’s outputSchema.
When you declare an output schema, servers return both: structuredContent for programmatic use and a JSON-serialized text block for backward compatibility with clients that do not read structured results.
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [{"type": "text", "text": "{\"on_hand\": 0, \"shippable\": false}"}],
"structuredContent": {"on_hand": 0, "shippable": false}
}
}
Be careful with resource links and embedded resources. A server can return a pointer to content you then fetch and feed to the model. That is a channel for injecting arbitrary text into your context from a party you may not control. Fetch only from sources you trust.
Saying it out loud. A tool result carries an array of content blocks — text, images, audio — and optionally a structured object validated against the tool’s output schema. When you declare an output schema, servers usually return both: the structured object for programmatic use and a JSON-serialized text block so older clients that don’t read structured results still work. The thing to be careful about is resource links and embedded resources: the server hands back a pointer, you fetch it, and you feed the contents to the model. That’s a channel for injecting arbitrary text into your context from a party you may not control, so only fetch from sources you trust.
Error handling
Two mechanisms, and the distinction is meaningful.
Protocol errors are JSON-RPC errors: unknown method, unknown tool, malformed arguments, server fault. They are a failure of the call, and typically the client handles them rather than the model.
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Unknown tool: chekc_stock. Check the tool name, or request an updated tool list."
}
}
Tool errors are successful protocol responses carrying "isError": true.
They mean the call reached the tool and the tool failed for a business reason — record not found, rate limit, permission denied.
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [{"type": "text",
"text": "Weather API rate limit exceeded. Wait 15 seconds before calling this tool again."}],
"isError": true
}
}
The distinction matters because tool errors go to the model and protocol errors generally do not.
Which means isError results are your last-prompt channel from Chapter 1, and you should write them accordingly.
Saying it out loud. There are two error mechanisms and the distinction is load-bearing. A protocol error is a JSON-RPC error — unknown method, unknown tool, malformed arguments — and it’s a failure of the call itself, so your client handles it, not the model. A tool error is a perfectly successful protocol response that carries an is-error flag, meaning the call reached the tool and the tool failed for a business reason: record not found, rate limit, permission denied. Why it matters: tool errors go into the model’s context and protocol errors generally don’t. So your is-error text is the last-prompt channel — it should say what went wrong and what to do next, like wait fifteen seconds before calling this again.
The current state: the 2026-07-28 revision
Here is where most tutorials, and a fair amount of code, are now out of date.
The specification revision dated 2026-07-28 made structural changes to the protocol core. If you learned MCP from material written before mid-2026, some of what you learned describes a model that no longer applies. The changes are worth understanding before you design anything, because they change what a good server looks like.
Saying it out loud. The honest framing for MCP is that it moves fast — the spec has had structural changes to its core, not just additions, so material written even a year earlier can describe a model that no longer applies. If someone asks me about mechanics, I’ll describe the shape of the change and then say I’d check the current specification revision before writing code against it, because that’s the actual professional behavior. What I’d know cold is the direction of travel: toward self-contained stateless requests, toward things a gateway can route and cache without parsing bodies, and away from server-initiated callbacks that require an open bidirectional channel.
The session is gone
This is the big one.
Previously, a client opened a connection, sent an initialize request, received the server’s capabilities, sent an initialized notification, and then made calls within that established session — tracked over HTTP by an Mcp-Session-Id header.
State lived on the server between requests.
That model is removed.
The initialize/initialized handshake and the Mcp-Session-Id header no longer exist.
Requests are now self-contained.
Each one carries what the server needs in its _meta field, under namespaced keys:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "check_stock",
"arguments": {"sku": "SKU-1002"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "my-harness", "version": "0.1.0"},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
Requests also carry an MCP-Protocol-Version: 2026-07-28 HTTP header.
Capability discovery still exists, but as an optional call: server/discover returns the server’s supported protocol versions, capabilities, and identity.
Servers must implement it; clients may skip it.
Why this matters to you as a builder: a stateless server can run behind an ordinary round-robin load balancer with no shared session store. Any instance can answer any request. Previously, scaling a remote MCP server meant sticky sessions or a shared state layer, which is a real operational burden and was one of the loudest complaints about the protocol.
The practical instruction is simple: design your server stateless from the first line. Do not keep per-connection state in memory. If a tool needs continuity across calls, put the continuity in an argument — a cursor, a job ID, a scratch table name — the way you would design any REST service.
Saying it out loud. The big change is that the session went away. It used to be that a client opened a connection, did an initialize handshake, got the server’s capabilities back, and then made calls inside that session, tracked over HTTP by a session-ID header — which meant state lived on the server between requests. Now requests are self-contained: everything the server needs rides along in the request’s metadata, and capability discovery is an optional separate call. Why that matters to you as a builder is operational — a stateless server sits behind an ordinary round-robin load balancer with no shared session store, and any instance can answer any request. Sticky sessions were one of the loudest complaints about the old model. So the practical instruction is: design stateless from the first line, keep no per-connection memory, and if a tool needs continuity, pass it as an argument — a cursor, a job ID — the way you would with any REST service. As always, check the current spec for the exact field names.
Multi Round-Trip Requests
Removing the session created a problem. Sampling and elicitation both worked by the server initiating a request back to the client mid-call, which requires an open bidirectional channel. No session, no channel.
The replacement is Multi Round-Trip Requests (MRTR), and it is a nicer design anyway.
When a tool needs something from the user or the model partway through, it does not block and call back.
It returns a result with resultType: "input_required", carrying:
inputRequests— a map of the things it needs (an elicitation prompt, a sampling request)requestState— an opaque blob the server hands out and the client must echo back unmodified
The client gathers the answers — showing a dialog, running a completion, whatever is appropriate — and re-issues the same call, now with inputResponses keyed to match inputRequests, plus the unmodified requestState.
Normal completions return resultType: "complete".
Notice the shape: the server carries no memory between the two calls.
Everything it needs to resume is in requestState, which the client held for it.
That is the same trick a stateless web service uses with a signed cookie, and it is what makes the whole thing load-balancer-safe.
Saying it out loud. Killing the session broke the features that depended on the server calling back into the client mid-request, because with no session there’s no open channel. The replacement is a multi-round-trip pattern, and it’s a nicer design anyway: instead of blocking and calling back, the tool returns a result that says input required, listing what it needs plus an opaque state blob. The client gathers the answers — shows a dialog, runs a completion — and re-issues the same call with the responses and that state blob echoed back unmodified. Notice the shape: the server keeps no memory between the two calls, everything it needs to resume was held by the client. It’s the same trick a stateless web service plays with a signed cookie, and it’s exactly what makes the whole thing load-balancer safe.
Routable headers
Streamable HTTP requests must now include two HTTP headers naming the operation:
Mcp-Method— the JSON-RPC method, e.g.tools/callMcp-Name— the specific target, e.g. the tool name
This looks trivial and is not.
It means a gateway, WAF, or rate limiter can route and meter MCP traffic by reading headers instead of parsing every JSON body.
You can now rate-limit tools/call on expensive_report differently from tools/list, at the edge, with off-the-shelf infrastructure.
For anyone trying to put MCP into an enterprise network, this closes a genuine gap.
Saying it out loud. Remote requests now carry HTTP headers naming the method and the specific target — which sounds trivial and really isn’t. It means a gateway, a WAF, or a rate limiter can route and meter MCP traffic by reading headers instead of parsing every JSON body. Concretely, you can rate-limit calls to one expensive tool differently from a cheap tool-list call, at the edge, with off-the-shelf infrastructure. For anyone trying to put MCP inside an enterprise network, that closes a genuine gap — before this, the only place you could enforce per-tool policy was inside the application.
Cache hints
tools/list, prompts/list, resources/list, resources/read, and resources/templates/list can now return ttlMs (how long the result stays fresh) and cacheScope ("public" or "private", following the HTTP Cache-Control model).
Small feature, real effect.
Tool lists were being re-fetched constantly; now a client can cache them for a stated duration, and a "public" scope tells a shared cache the result is not user-specific.
Saying it out loud. The list endpoints can now return cache hints — how long a result stays fresh, and whether the cache scope is public or private, borrowing the HTTP cache-control model. Small feature, real effect: tool lists were being re-fetched constantly, and every one of those round trips was latency on the critical path of a conversation. A public scope also tells a shared cache the result isn’t user-specific, so one fetch can serve many clients. The tradeoff to keep in mind is staleness versus chattiness — if a server changes its tool list mid-TTL, your client is working from an out-of-date view.
Deprecations
Roots, Sampling, and Logging are deprecated. So is the legacy HTTP+SSE transport. The specification commits to a minimum twelve-month window between deprecation and eligibility for removal, with an expedited ninety-day path reserved for security issues.
Sampling and elicitation functionality moves to MRTR. Roots never worked well — servers were only ever asked to “SHOULD respect” the boundary, with no enforcement, so it was never a security control regardless.
Tasks — the mechanism for long-running work — moved out of the experimental core and into an extension, io.modelcontextprotocol/tasks, using poll-based tasks/get rather than a blocking tasks/result.
Change notifications consolidated into a single subscriptions/listen stream that clients opt into per notification type.
Authorization also hardened: RFC 9207 issuer validation is required, Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents, and there is a new application_type parameter for desktop and CLI clients.
Saying it out loud. Several client-side capabilities are deprecated — roots, sampling, and logging — along with the legacy SSE transport, and their functionality either moved into the multi-round-trip mechanism or went away. The spec commits to at least a twelve-month window between deprecation and removal, with a much shorter expedited path reserved for security issues, so you get warning but not indefinite warning. The interesting one is roots, which was supposed to bound which filesystem paths a server could touch: servers were only ever asked to “SHOULD respect” the boundary, with nothing enforcing it, so it was never a security control in the first place. That’s the general lesson — a hint the other side is trusted to honor is documentation, not a control.
What this means for how you build today
Four concrete instructions.
Write stateless servers. No per-connection memory. If continuity is needed, pass it explicitly.
Do not build on Sampling, Roots, or Logging. They have a sunset date. Use MRTR where you need mid-call input.
Check your SDK version. The Python SDK’s 1.x line implements the older session model; 2.0 betas implement the new one and can serve both revisions from one endpoint. Know which you are running before you debug a version mismatch.
Expect a transition period. Servers and clients in the wild speak a mix of revisions for a while. Version negotiation is a thing you will actually have to think about.
Saying it out loud. Four practical instructions for building today. Write stateless servers with no per-connection memory, and pass continuity explicitly. Don’t build on the deprecated client-side capabilities; use the multi-round-trip path where you need mid-call input. Check your SDK version, because the older major line implements the old session model and the newer one implements the new one, and a version mismatch looks exactly like a bug until you notice. And expect a transition period — servers and clients in the wild will speak a mix of revisions for a while, so version negotiation is something you’ll actually have to think about rather than assume away.
For and against
MCP is genuinely useful and genuinely costly. Both halves deserve a fair hearing.
What it buys you
Integration work stops being quadratic. The \(N + M\) argument is real, and a public server ecosystem plus a central MCP Registry means a lot of adapters are already written.
Tools become discoverable at runtime. tools/list means a host can pick up new capabilities without a redeploy. Powerful, and — see below — also a risk.
Your architecture decouples. Swap the model, swap the backend, keep the interface. Tools become an independent, versioned layer rather than code welded into your agent.
It gives you a place to put governance. One server in front of a system is one point where authentication, authorization, rate limiting, and audit logging can live, applied uniformly to every agent that connects.
Saying it out loud. What you get: integration stops being quadratic, and there’s a public registry so a lot of adapters are already written. Tools become discoverable at runtime, so a host can pick up new capabilities without a redeploy. Your architecture decouples — swap the model, swap the backend, keep the interface — so tools become an independent versioned layer instead of code welded into your agent. And it gives you one place to put governance: a single server in front of a system is a single point where auth, rate limiting, and audit logging apply uniformly to every agent that connects. That last one is usually what actually sells it internally.
What it costs you
A trust boundary you did not have before. When you connect to a third-party server, you are letting someone else’s text into your model’s context and letting your model’s requests reach their code. Tool descriptions are prompts, so a malicious description is a prompt injection with a direct line to your agent’s decision-making. A server can also change its tool list at any time — the poetry agent that connects to a books server for quotes can wake up one morning to find the server has added a purchasing tool. Mitigations: allowlist servers and tool names, pin tool definitions by hash and alert on change, prefer servers you host yourself, and put a gateway in front of everything.
Context window bloat. Every tool from every connected server has its definition and schema loaded into the model’s context on every request. Connect five servers with fifteen tools each and you have spent thousands of tokens before the user says anything — on every turn. Worse, reasoning quality degrades when the tool list gets long: the model starts picking irrelevant tools or losing track of the request. This is the scaling limit nobody has solved cleanly. The likely direction is retrieval over tools — search a tool index for the handful relevant to the current task and load only those — which introduces its own attack surface if someone can write to that index.
Versioning. Servers evolve. Tool signatures change. Nothing in the protocol pins a server to a version your agent was tested against. You have to build that discipline yourself, and the 2026-07-28 revision means you are now also managing protocol-level version skew.
Latency. Every call is now a network hop plus JSON-RPC framing, where an in-process function call was neither. For a stdio server on the same machine this is negligible. For a remote server behind a gateway, it is tens of milliseconds per call, multiplied by every call in a multi-step task.
Debugging. When an agent misbehaves and the tool lives in someone else’s process, the trace goes cold at the boundary. Log every request and response at the client, capture server stderr, and learn the MCP Inspector before you need it — not during an incident.
Saying it out loud. The costs are real and I’d name two above the rest. First, a trust boundary you didn’t have: connecting to a third-party server means someone else’s text enters your model’s context, and since tool descriptions are prompts, a malicious description is prompt injection wired directly into your agent’s decision-making — and the server can change its tool list at any time, so the books server you connected for quotes can wake up one day having added a purchasing tool. Mitigate by allowlisting servers and tool names, pinning definitions by hash and alerting on change, and putting a gateway in front. Second, context bloat: every tool from every connected server has its schema in context on every request, so five servers with fifteen tools each burns thousands of tokens before the user says a word, and reasoning quality degrades as the tool list grows. That’s the scaling limit nobody has cleanly solved — retrieval over tools is the likely direction, and it brings its own attack surface if anyone can write to the index.
When to use it
Use MCP when the tool crosses a boundary: another team’s system, another company’s product, another process on the user’s machine, or a capability you want several agents to share.
Do not use it when the tool is three lines of Python that lives in the same file as your agent. A decorator is faster, easier to debug, and has no trust boundary. The framework you built in Chapter 3 is not obsoleted by MCP — most production agents run both, local tools for local work and MCP clients for everything that crosses a line.
Saying it out loud. My rule is: use MCP when the tool crosses a boundary — another team’s system, another company’s product, another process on the user’s machine, or a capability several agents should share. Don’t use it when the tool is three lines of Python living in the same file as your agent, because a decorator is faster, easier to debug, and has no trust boundary at all. Those aren’t competing choices either; most production agents run both, local tools for local work and MCP clients for everything that crosses a line. The cost you’re paying for crossing that line is a network hop plus framing on every call, which is negligible over stdio and tens of milliseconds per call through a remote gateway — multiplied by every step of a multi-step task.
What you should be able to do now
- Explain the \(N \times M\) problem with real numbers and say precisely which roles host, client, and server play in solving it.
- Read a raw JSON-RPC MCP exchange and identify the method, the arguments, whether it succeeded, and whether a failure was a protocol error or a tool error.
- Write an MCP tool definition with a description that functions as a prompt, complete input and output schemas, and honest annotations — while explaining why annotations are not a security control.
- State what the 2026-07-28 revision changed — stateless requests, MRTR,
Mcp-Method/Mcp-Namerouting headers,ttlMs/cacheScope, and the Roots/Sampling/Logging deprecations — and design a server that is stateless from the start. - Argue both sides of adopting MCP for a specific integration, including context bloat and the third-party trust boundary, and decide when a local tool is the better answer.
Further reading
- MCP specification, current revision — https://modelcontextprotocol.io/specification/2026-07-28
- “The 2026-07-28 Specification,” the release announcement — https://blog.modelcontextprotocol.io/posts/2026-07-28/
- Beta SDKs for the 2026-07-28 spec — https://blog.modelcontextprotocol.io/posts/sdk-betas-2026-07-28/
- MCP tools reference — https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- MCP transports — https://modelcontextprotocol.io/specification/2026-07-28/basic/transports
- MCP Registry — https://github.com/modelcontextprotocol/registry
- MCP Inspector, the debugging tool — https://github.com/modelcontextprotocol/inspector
- JSON-RPC 2.0 specification — https://www.jsonrpc.org/specification
- Language Server Protocol, the design MCP borrows from — https://microsoft.github.io/language-server-protocol/