Every team that ships an agent to production walks into the same wall. The model is excellent, the prompt is sharp, but the agent is only useful when it touches the real world — Jira, the data lake, the ERP, that internal service nobody documented. And that is when you discover 80% of the effort is not AI at all: it is writing yet another bespoke connector for every tool, in every framework, with a slightly different tool calling shape. Switch models and rewrite it. Switch frameworks and rewrite it again. The Model Context Protocol (MCP) exists to kill exactly that rework.
2026 update On July 28, 2026, MCP published revision 2026-07-28 — the largest change since launch: nine major changes, twelve minor ones, and six formally deprecated features. The protocol went stateless, the initialize handshake and the Mcp-Session-Id header were removed, the GET endpoint gave way to subscriptions/listen, SSE resumability is gone, extensions became first-class, and authorization moved closer to real-world OAuth 2.x. There are breaking changes here. This article covers the before and after — and includes a migration checklist.
What is the Model Context Protocol?
MCP is an open protocol that standardizes how an AI agent discovers and uses external capabilities — tools, data, and ready-made workflows. The analogy that stuck is the USB-C of AI: instead of a proprietary cable for every device, one connector, and anything that speaks the same protocol plugs in.
In practice it defines three roles:
- Host — the application where the agent lives (your app, an IDE, a copilot).
- MCP client — the component inside the host that speaks the protocol, typically one per connected server.
- MCP server — the process that exposes capabilities. It is the one that knows how to talk to Jira, Databricks, or your internal API.
And the server exposes three primitives:
| Primitive | What it is | Who controls it |
|---|---|---|
| Tools | Functions the model can invoke (search, create, compute) | The model decides to call |
| Resources | Readable data addressed by URI (files, records, config) | The application decides to attach |
| Prompts | Ready-made workflows and templates exposed as commands | The user decides to trigger |
That distinction matters more than it looks: a tool is an action with side effects, a resource is a read. Conflating the two is the number one design mistake in MCP servers — and it is what turns a helpful agent into a dangerous one.
The problem it solves
Without a standard, integrating M agents with N tools is an M × N problem: every pair needs its own connector, with its own authentication, schema shape, and error handling. With MCP it becomes M + N: each agent speaks the protocol once, each tool exposes the protocol once, and every combination comes for free.
It is the same economics ODBC brought to databases in the 90s and the Language Server Protocol brought to code editors. Nobody has written one driver per (application, database) pair since — and this is exactly why.
Three concrete pains disappear:
- Framework lock-in — the MCP server you write today serves an agent in LangGraph, in Agent Framework, and in the copilot that does not exist yet.
- Runtime discovery — the agent asks the server which tools exist and gets a JSON schema for each. You add a capability without redeploying the agent.
- A concentrated security surface — authentication, scope, and audit live in one place (the server), not scattered across twenty homemade connectors.

How it works — step by step
- Expose the capabilities — on the server, declare each tool with a name, a description, and an input schema. The description is not decoration: it is what the model reads to decide whether and when to call.
- Connect the client — the host opens a connection to the server over one of two transports: stdio (local process, ideal for desktop and IDE) or Streamable HTTP (remote server, a single
POST /mcpendpoint). - Discover — the client calls
tools/listand gets the catalog with schemas. As of2026-07-28, the response carriesttlMsandcacheScope, so the client knows how long it may cache that catalog instead of asking again every turn. - Let the model choose — the agent receives the available tools alongside the task and decides which to invoke, with which arguments.
- Execute with approval — the client calls
tools/call. Destructive or irreversible actions go through human confirmation before they leave. - Return the result to context — the output comes back as content (and, optionally, schema-validated
structuredContent) and the model continues reasoning.
[ Agent ] "what is the balance on contract 4471?"
|
v
+----------------+ tools/list (cacheable: ttlMs)
| MCP client | <------------------------------+
+----------------+ |
| POST /mcp Mcp-Method: tools/call |
| Mcp-Name: get_contract |
v |
+----------------+ +--------------+
| MCP server | -----------------------> | ERP / Lake |
+----------------+ SQL / REST +--------------+
|
v
[ result ] or [ input_required -> confirm? ]
What changed in revision 2026-07-28
If you already run an MCP server, this is the section that matters. The previous revision was 2025-11-25, and the jump is a big one: nine major changes, twelve minor ones, six formally deprecated features and — for the first time — a public lifecycle policy for protocol features.
There is a single thread running through all of it: take state out of the transport. Almost everything else follows from that.
The protocol went stateless
Before, every conversation started with an initialize handshake, followed by notifications/initialized, and the server returned an Mcp-Session-Id that pinned the client to that instance. Scaling horizontally required sticky sessions and a shared session store.
Now initialize and Mcp-Session-Id are gone. Every request carries what it needs in _meta:
_meta key |
What it carries |
|---|---|
io.modelcontextprotocol/protocolVersion |
the protocol version for that request |
io.modelcontextprotocol/clientCapabilities |
client capabilities |
io.modelcontextprotocol/clientInfo |
client identity |
io.modelcontextprotocol/serverInfo |
server identity (in the result’s _meta) |
A version mismatch is now an explicit UnsupportedProtocolVersionError instead of a handshake that fails in the dark. And the list endpoints (tools/list, resources/list, prompts/list) no longer vary per connection, which finally makes client-side caching trustworthy.
To pick a version before any call there is the new server/discover, which every server MUST implement: it advertises supported versions, capabilities, and identity. On stdio it also doubles as a backward-compatibility probe against older servers.
The practical effect is large: any request can land on any instance. Plain round-robin does the job, and an MCP server now fits comfortably on serverless platforms that scale to zero — Azure Functions on the consumption plan, for instance — which used to fight with session affinity.
A stateless protocol does not mean a stateless application. If your server needs to carry context across calls, the spec now prescribes the explicit pattern: the server mints a handle (basket_id, job_id), returns it as a result, and the model passes it back as an ordinary tool argument on the next call. State becomes visible to the model instead of hidden in transport metadata — which is more powerful and far easier to debug.
The GET endpoint is gone: enter subscriptions/listen
The HTTP GET endpoint and the resources/subscribe / resources/unsubscribe pair were replaced by a single method: subscriptions/listen, a long-lived POST-response stream where the client explicitly opts in to the notification types it wants — toolsListChanged, promptsListChanged, resourcesListChanged, and resourceSubscriptions. The server acknowledges the subscription and tags every notification with io.modelcontextprotocol/subscriptionId.
Request-scoped notifications (notifications/progress, notifications/message) keep flowing on the response stream of the request they belong to — not on that channel. It is a clean split between “tell me when the catalog changes” and “tell me how this call is going”.
A broken stream is now a lost request
SSE resumability is out: there is no more Last-Event-ID and no message redelivery. If the response stream drops, the in-flight request is lost and the client must re-issue it with a new request id.
That changes how you design long-running tools, in two very concrete ways:
- Idempotency stopped being a best practice and became a requirement. Re-issues will happen; your write tool has to tolerate the second call without duplicating the effect.
- Long work belongs to the Tasks extension, not to a synchronous call holding a stream open for minutes.
Asking the user mid-call changed shape
Elicitation, roots/list, and sampling/createMessage were server-initiated requests. That whole mechanism is gone, replaced by the Multi Round-Trip Requests (MRTR) pattern.
Every result now carries a required resultType field:
"complete"— an ordinary result;"input_required"— the server returnsinputRequestswith what it needs to know, plus an opaquerequestState.
The client gathers the answers and re-issues the original call with inputResponses and the same requestState. Because everything the server needs is in the payload, the retry can land on any instance. For backward compatibility, a result from an older server without resultType must be treated as "complete".
Gone with it: the notifications/elicitation/complete notification and the elicitationId field. Anyone who needs to correlate an out-of-band interaction across retries now encodes their own identifier in requestState.
And one rule of etiquette became mandatory: the server may only initiate requests while processing a client request. The user is never interrupted out of nowhere.
The traffic became operable
Mcp-MethodandMcp-Nameheaders are now required on Streamable HTTP POSTs. Your gateway routes, throttles, and measures per operation without opening the request body — in API Management, that is the difference between a one-line policy and a script.x-mcp-headerlets you promote tool parameters to custom HTTP headers.ttlMsandcacheScopebecame required on results fromtools/list,prompts/list,resources/list,resources/read, andresources/templates/list, through the newCacheableResultinterface.ttlMsis a freshness hint in milliseconds;cacheScope("public"or"private") says whether a shared intermediary may cache that result.- Deterministic ordering in
tools/listbecame a recommendation — and it is not cosmetic: a stable list improves the model’s prompt cache hit rate, and that shows up on the bill. - W3C Trace Context (
traceparent,tracestate,baggage) standardized in_meta— the trace crosses host, SDK, MCP server, and whatever comes next, and shows up as a single span tree in your OpenTelemetry backend.
Methods that simply disappeared
ping, logging/setLevel, and notifications/roots/list_changed were removed. Log level is now set per request, via io.modelcontextprotocol/logLevel in _meta — and the server MUST NOT emit notifications/message for requests that did not ask for logs. Less noise on the stream, fewer egress bytes, less cost.
Extensions became first-class citizens
ClientCapabilities and ServerCapabilities gained an extensions field. Extensions are identified by reverse-DNS, negotiated as capabilities, and versioned outside the spec — that is how MCP intends to evolve without bloating the core. Two are already official:
- MCP Apps — the server ships HTML interfaces the host renders in a sandboxed iframe.
- Tasks (
io.modelcontextprotocol/tasks) — moved out of the experimental core into a redesigned extension: the blockingtasks/resultgave way to polling withtasks/get;tasks/updatewas added so the client can feed information in during execution;tasks/listwas removed; and the server may return a task handle without per-request opt-in.
Authorization: closer to real-world OAuth
- The authorization server SHOULD include the
issparameter in the response (RFC 9207), and the MCP client MUST validate it against the recorded issuer before redeeming the code — closing the door on mix-up attacks. - The client must declare
application_typeduring dynamic registration, avoiding redirect URI conflicts with OpenID Connect. - Credentials are bound to the issuer that minted them: key them by issuer, never reuse them with a different authorization server, and re-register when it changes.
- And the change of direction: Dynamic Client Registration (RFC 7591) was deprecated as a registration mechanism in favor of Client ID Metadata Documents. DCR still works for backward compatibility, but it is no longer the recommended path.
Deprecation now has written rules
MCP adopted a feature lifecycle policy: every feature is Active, Deprecated, or Removed, with a minimum twelve-month window between deprecation and removal, and a public registry of what is deprecated. That is the difference between “this will break someday” and “you have a year and a date”.
On the list: Roots (use tool parameters, resource URIs, or server configuration), Sampling (call the LLM provider API directly), Logging (stderr on stdio, or OpenTelemetry), the old HTTP+SSE transport (migrate to Streamable HTTP), the "thisServer" / "allServers" values of includeContext, and DCR, mentioned above.
Details that break clients quietly
inputSchemaandoutputSchemanow accept full JSON Schema 2020-12 (oneOf,$ref,$defs), andstructuredContentaccepts any JSON value. You gain expressiveness — and the obligation to resolve$ref.- A missing resource moved from
-32002to the standard JSON-RPC-32602(Invalid Params). - The error range was partitioned:
-32000to-32019stays with implementations, and-32020to-32099is reserved for the spec. The new codes were renumbered:HeaderMismatch-32001→-32020,MissingRequiredClientCapability-32003→-32021,UnsupportedProtocolVersion-32004→-32022.
Migration checklist
| What you have today | What to do |
|---|---|
initialize + Mcp-Session-Id |
Remove them; send version and capabilities in _meta on every request |
| Sticky sessions on the load balancer | Turn them off; any instance serves any request |
| State kept per session | Mint an explicit handle and pass it back as a tool argument |
GET endpoint, resources/subscribe |
Move to subscriptions/listen, with per-type opt-in |
Reconnect via Last-Event-ID |
Re-issue the request with a new id; make tools idempotent |
roots/list, sampling/createMessage, elicitation |
Adopt MRTR: resultType, inputRequests, requestState |
logging/setLevel and ping |
Per-request log level in _meta; health check on your own HTTP endpoint |
| List results without caching | Fill in ttlMs and cacheScope; order tools/list deterministically |
| DCR (RFC 7591) | Evaluate Client ID Metadata Documents |
Handling of -32002 |
Start handling -32602 |
None of this is optional if you operate a remote server. The good news is that almost every change trades infrastructure complexity for explicitness in the payload — and an explicit payload is exactly what you can log, cache, version, and audit.
Building and hosting an MCP server on Azure
A minimal server in Python fits in twenty lines. What separates the prototype from production is where it runs and who can call it.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("contracts")
@mcp.tool()
def get_contract(number: str) -> dict:
"""Returns registration data for a contract by number.
Use when the user mentions a contract number."""
return repository.get(number) # read, no side effects
@mcp.tool()
def terminate_contract(number: str, reason: str) -> dict:
"""Terminates a contract. IRREVERSIBLE ACTION — requires confirmation."""
return repository.terminate(number, reason)
@mcp.resource("contract://{number}/clauses")
def clauses(number: str) -> str:
"""Clause text — read-only data, attached by the application."""
return repository.clauses(number)
Notice what matters: the docstring is your contract with the model. It says what the tool does and when to use it. A vague description is the root cause of most agents that call the wrong tool.
To ship it, you have four paths on Azure, from simplest to most controlled:
- Azure Functions — a remote MCP server with scale-to-zero. Best value for intermittently used servers, and it integrates with managed identity so you reach resources without secrets in code.
- Azure Container Apps — when you want the container your way. With internal ingress and a dedicated subnet, the server is reachable only from inside the VNet.
- Azure API Management — the AI gateway in front of the server. This is where you apply authentication, rate limiting, per-consumer quotas, content policies, and telemetry. With the
Mcp-MethodandMcp-Nameheaders, you can write per-operation policy — blockingtools/callforterminate_contractfor a specific consumer, for example. - Foundry Agent Service — the other side of the counter: here you connect the agent to MCP servers (yours or third-party) without writing the client plumbing.
Add to that the Azure MCP Server, which exposes Azure’s own resources as tools — useful for operations and diagnostics agents.
Best practices for production
- Treat the tool description as production code. It is what the model reads to decide. Be specific about when to use it and when not to.
- Separate reads from writes and require human confirmation on anything irreversible.
input_requiredexists for this. - Least privilege, per server. One token per MCP server, with the smallest possible scope. A broad token is the fuel for a confused deputy attack, where the server acts with more privilege than the user has.
- Treat tool output as untrusted input. Tool poisoning — malicious instructions hidden in a tool description or return value — is currently the most exploited vector. A third-party tool description is hostile content until proven otherwise.
- Allowlist and review third-party servers. An approved server can change behavior in an update (rug pull). Pin versions and review schema changes.
- Validate the
Originheader and never expose a local server on0.0.0.0. A local stdio server listens onlocalhost— and only there. - Instrument with OpenTelemetry from day one. With Trace Context standardized, you can answer “which tool did the agent call, with which arguments, and what did it cost” — which is exactly what audit will ask.
- Cache
tools/listhonoringttlMs. Listing tools every turn wastes tokens and latency.
Frequently asked questions (FAQ)
Does MCP replace the model’s function calling?
No. They are different layers. Function calling is the model’s ability to emit a structured call; MCP is the protocol that standardizes how that tool is discovered, described, and executed by an external process. In practice MCP feeds function calling with a standardized catalog.
Do I need to migrate my server to revision 2026-07-28 now?
If it is remote and you want to scale, yes — it is worth the effort, because the need for sticky sessions and a shared session store disappears. Since there are breaking changes (no more initialize, Mcp-Session-Id, or SSE resumability; a new subscriptions/listen; a different elicitation flow), plan a window, work through the migration checklist, and validate against the conformance suite. Local stdio servers feel far less impact. And remember: the new lifecycle policy guarantees at least twelve months for anything merely deprecated — what is urgent is what was removed.
When should I use stdio versus Streamable HTTP?
stdio for servers running on the user’s machine alongside the host (IDE, desktop, CLI) — no network, no exposed port. Streamable HTTP for servers shared by many users or that need to live close to the data, inside your Azure environment.
Is MCP secure?
The protocol gives you the tools (OAuth 2.x, scopes, explicit consent, audit), but security is your architecture’s job. Real incidents do not come from the protocol: they come from tokens with too broad a scope, unreviewed third-party servers, and missing human approval on destructive actions.
Is it worth writing an MCP server for an internal API only one agent uses?
It is worth it if you expect more than one consumer over time — and you usually do. The marginal cost of exposing it over MCP instead of a homemade connector is low, and the payoff shows up with the second agent, the second framework, or the day another team wants the same capability.
Conclusion
MCP stopped being a curiosity and became infrastructure. With revision 2026-07-28, it stopped demanding infrastructure tricks to scale: it runs on ordinary HTTP, behind an ordinary load balancer, with ordinary caching, routing, and tracing. That changes the math for anyone who was waiting for the standard to mature.
The architecture decision left for you is no longer “should I adopt MCP?”. It is what you expose, at which scope, and under which approval — because every tool you publish is a door a model can open on its own.
👉 If you are designing the integration layer for your agents — especially in a regulated environment, where scope and audit are not optional — MCP is the decision that saves you from rewriting connectors every time you switch models. Want to talk about MCP and agent architecture? Reach out on LinkedIn.
