MCP in Production: What the 2026-07-28 Stateless Spec Actually Means for Your Platform
Sources
- MCP specification 2026-07-28 — key changes
- MCP architecture overview
- MCP Streamable HTTP transport (2026-07-28)
- MCP server/discover RPC (2026-07-28)
- MCP deprecated features registry (Roots, Sampling, Logging, HTTP+SSE)
- MCP versioning and per-request protocol negotiation
- MCP security best practices (confused deputy, tool poisoning, rug pull)
- MCP authorization: OAuth 2.1 and RFC 9728 Protected Resource Metadata
- MCP client best practices: progressive tool discovery, programmatic tool calling
- MCP Enterprise-Managed Authorization extension (ID-JAG)
- MCP Registry (preview): server.json metadata
- Python SDK (modelcontextprotocol/python-sdk) — v2.2.0
- SEP-2575: stateless MCP, server/discover, subscriptions/listen
- SEP-2322: Multi Round-Trip Requests (MRTR) and resultType
- SEP-2243: Mcp-Method / Mcp-Name request headers
- SEP-2577: deprecating Roots, Sampling, Logging
- SEP-2549: ttlMs and cacheScope on list results
- MCP Apps extension (ext-apps)
Every platform team that shipped a Model Context Protocol server in the last two years shipped it against a protocol that no longer exists. Revision 2026-07-28 of the MCP specification deleted the initialize handshake, deleted protocol-level sessions, deleted the SSE GET stream, deprecated Sampling, Roots, and Logging, and moved version negotiation into the body of every request. If your mental model of MCP is “JSON-RPC with an init dance and a session header,” that model is now dead and everything you deploy on top of it will behave strangely in production.
We rebuilt a small tool server against the new spec using the official Python SDK (v2.2.0, released 2026-09-07), drove it over Streamable HTTP, and captured the actual wire traffic with raw POSTs. This guide is the result: what changed, why the platform-engineering implications are bigger than the diff suggests, and the manifests, OAuth wiring, and failure modes you need to run MCP servers as production infrastructure rather than desktop-app curiosities.
TL;DR
- MCP is now stateless at the protocol level. No
initialize, noMcp-Session-Id, no GET stream. Every request is a self-contained POST carrying its protocol version in_meta. We verified this on the wire: the sametools/callworks with zero prior handshake. - Version negotiation moved to per-request
_metaplus a new mandatoryserver/discoverRPC. A mismatch returnsUnsupportedProtocolVersionError(error code-32022) — we triggered it live. - New required headers on every POST:
Mcp-MethodandMcp-Name, enforced withHeaderMismatch(-32020). We confirmed the SDK rejects atools/callwhoseMcp-Namedoes not match the tool being called — this will break naive proxies and hand-rolled clients that assume header-less JSON-RPC. - Sampling, Roots, and Logging are deprecated with a twelve-month window (earliest removal 2027-07-28). New server builds should not adopt them. If your agent host depends on server-initiated sampling, you now own that LLM call chain directly.
- The production surface is now horizontal. Stateless requests mean boring, scalable HTTP semantics: any replica can serve any request, load balancing is trivial, and retries are safe. But per-request context (who is calling, what tenant, what trace) must now ride in headers and
_meta, because there is no session to hang it on. - Security got teeth. OAuth 2.1 with RFC 9728 Protected Resource Metadata, RFC 9207
issvalidation on code redemption, issuer-keyed credential storage, and a documented confused-deputy attack against MCP proxies that use static client IDs. Enterprise fleets get the Enterprise-Managed Authorization extension (ID-JAG) so Okta/Azure AD policy, not per-user consent, controls server access.
The Structural Shift: From Chat-with-Docs to Stateless Agent Infrastructure
The first era of MCP — the one most tutorials and most production deployments still target — was architecturally a desktop-app protocol. A host like Claude Desktop or VS Code launched local servers over stdio, performed an initialize handshake, and kept a long-lived session per server. That design was fine for one developer’s laptop. It was hostile to everything a platform team actually needs: horizontal scaling, load balancing across replicas, transparent retries, and clean multi-tenancy. Sticky sessions on a stateful protocol means every replica holds per-connection state, every load balancer needs session affinity, and every network blip is a session-resumption problem.
Revision 2026-07-28 reworks MCP into what the Streamable HTTP transport now plainly states: the server operates as an independent process handling multiple clients, every JSON-RPC message is its own HTTP POST, and a broken stream just means the client re-issues the request with a new ID. Sessions, event replay, and the GET-side channel are gone. List endpoints no longer vary per connection. Cross-call state, when a server genuinely needs it, moves into explicit server-minted handles passed as ordinary tool arguments — state by value, not by session.
That is the structural shift: MCP stopped being a conversation protocol and became a request/response protocol with an agreed capability vocabulary. For platform engineers this lands in three places:
- Load balancing: any replica can serve any request. No affinity. We ran the same server behind repeated raw POSTs with no session state whatsoever.
- Identity and tenancy: with no session to hang context on, tenant and caller identity must travel per-request (OAuth token claims,
x-mcp-headercustom headers from tool parameters,_metatrace context). This is a forced maturity upgrade for servers that previously ignored who was calling. - Observability: the spec now documents OpenTelemetry trace context propagation conventions for
_metakeys (traceparent,tracestate,baggage), and the Python SDK ships anOpenTelemetryMiddleware. Distributed tracing is no longer an SDK-by-SDK afterthought.
There is a second shift worth naming before anyone schedules a migration. The client side of the old protocol — servers asking the host to sample an LLM (sampling/createMessage), clients advertising filesystem roots — is being wound down. Roots, Sampling, and Logging are all Deprecated as of 2026-07-28, with earliest removal at the first revision on or after 2027-07-28. The migrations the spec prescribes are blunt: pass paths via tool parameters or config instead of Roots; call LLM provider APIs directly instead of Sampling; log to stderr or OpenTelemetry instead of the MCP Logging feature. The protocol is narrowing to “server exposes tools/resources/prompts; client calls them” — and that narrowing is exactly what makes it deployable as platform infrastructure.
Architectural Blueprint
Here is the topology this guide builds toward: a fleet of stateless MCP tool servers behind an ingress, OAuth 2.1 in front, per-request identity and trace context, and a host-side gateway that handles discovery, caching, and progressive tool loading.
flowchart TB
subgraph HOST["MCP Host (Agent Runtime)"]
GW["Host-side MCP client(s)"]
DISC["server/discover probe
(version + capabilities, optional)"]
GW --> DISC
end
subgraph EDGE["Platform Edge"]
ING["Ingress / Gateway
TLS, Origin validation, rate limit"]
AUTHN["OAuth 2.1 resource server
RFC 9728 PRM discovery
token: audience-bound, issuer-keyed"]
ING --> AUTHN
end
subgraph FLEET["Stateless MCP server fleet (n replicas)"]
S1["Replica 1
POST /mcp"]
S2["Replica 2
POST /mcp"]
S3["Replica n
POST /mcp"]
OTEL["OpenTelemetry middleware
traceparent in _meta"]
end
subgraph BACKING["Backing systems"]
TOOLS["Databases, CI, cloud APIs
(per-tool scoped credentials)"]
REG["MCP Registry
(server.json metadata, preview)"]
end
GW -->|"POST tools/call
_meta: protocolVersion 2026-07-28
headers: Mcp-Method, Mcp-Name"| ING
AUTHN --> S1
AUTHN --> S2
AUTHN --> S3
S1 --> TOOLS
S2 --> TOOLS
S3 --> TOOLS
OTEL -.-> S1
REG -.->|"publish server metadata"| S1
Three properties of this diagram are the whole story. First, there are no session arrows: every path from host to replica is a self-contained POST, so the fleet scales horizontally and survives replica churn without draining sessions. Second, identity is enforced at the edge (OAuth 2.1 per the authorization model), not inside the tool. Third, discovery and caching live host-side: the host probes server/discover once, caches list results using ttlMs/cacheScope, and only injects tool definitions into context on demand — the progressive discovery pattern the docs now recommend for fleets of hundreds of tools.
The New Wire Format, Captured Live
Everything below is real traffic from our test rig: the incident-ops server (two tools: list_oncall, ack_incident) built on Python SDK v2.2.0, served over Streamable HTTP, probed with raw POSTs. No SDK client involved in these captures — this is the wire.
First, capability discovery. server/discover is the new mandatory RPC: no handshake, no session, one POST in, one JSON response out:
{
"jsonrpc": "2.0",
"id": "d1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "probe", "version": "0.0.1" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
{
"jsonrpc": "2.0",
"id": "d1",
"result": {
"cacheScope": "private",
"capabilities": {
"prompts": { "listChanged": true },
"resources": { "listChanged": true, "subscribe": true },
"tools": { "listChanged": true }
},
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"ttlMs": 0,
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "incident-ops", "version": "" }
}
}
}
Two production details in that response. The resultType field is new and mandatory on every result — "complete" or "input_required" (more on that below). And ttlMs/cacheScope are the SEP-2549 cache hints that let hosts cache list responses instead of re-listing tools every turn; this server returns ttlMs: 0 (no freshness hint) with cacheScope: private.
Now the workhorse. A tool call is one POST. The headers are required, and the Mcp-Name header must name the tool being called — not the client:
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
Mcp-Method: tools/call
Mcp-Name: list_oncall
MCP-Protocol-Version: 2026-07-28
{
"jsonrpc": "2.0",
"id": "c1",
"method": "tools/call",
"params": {
"name": "list_oncall",
"arguments": { "team": "sre" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "probe", "version": "0.0.1" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
{
"jsonrpc": "2.0",
"id": "c1",
"result": {
"content": [
{ "text": "{\n \"primary\": \"carol\",\n \"secondary\": \"dave\"\n}", "type": "text" }
],
"isError": false,
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "incident-ops", "version": "" }
}
}
}
And the error path — a tool raising ToolError returns HTTP 200 with isError: true, not an HTTP error. This distinction matters enormously for your alerting: a fleet of MCP servers can be perfectly green on HTTP 5xx metrics while every tool call fails semantically. Monitor isError rates, not just status codes:
{
"jsonrpc": "2.0",
"id": "c2",
"result": {
"content": [
{ "text": "Error executing tool list_oncall: Unknown team: does-not-exist. Known teams: platform, sre", "type": "text" }
],
"isError": true,
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "incident-ops", "version": "" }
}
}
}
What the SDK rejects at the door
The spec’s new enforcement points are not decorative. We probed them deliberately:
| Probe (raw POST against /mcp) | Actual response | Spec reference |
|---|---|---|
No MCP-Protocol-Version header | 400 “Bad Request: Missing session ID” — the SDK routes header-less requests to the legacy 2025-11-25 session transport (era routing) | Streamable HTTP: protocol version header |
Header version ≠ body _meta version | 400 {"code": -32020, "message": "mcp-protocol-version header does not match the request envelope's protocol version"} | SEP-2243, HeaderMismatch |
Mcp-Name ≠ tool name param | 400 {"code": -32020, "message": "mcp-name header does not match the request body's 'name' parameter"} | SEP-2243, HeaderMismatch |
Unsupported version in _meta | 400 with UnsupportedProtocolVersionError semantics; client retries or surfaces error; server lists supported versions | server/discover + versioning |
Read the first row twice if you operate a fleet. The Python SDK ships two HTTP stacks behind one endpoint and routes by the presence and value of the MCP-Protocol-Version header: header absent (or a legacy value) lands you on the 2025-11-25 session-based transport with its Mcp-Session-Id machinery; header present with a modern value lands you on the single-exchange 2026-07-28 path. That is backward-compatibility done right, but it means mixed-version traffic in the same fleet silently gets different semantics, different failure modes, and different observability. Pin your fleet, pin your clients, and alert on era-mixing.
Build the Server: Real Code, Real Run
Our test server is deliberately boring — an incident-operations server with two tools — because boring is what you want when validating a protocol revision. The entire implementation on SDK v2.2.0:
import logging
import sys
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
logger = logging.getLogger(__name__)
mcp = MCPServer("incident-ops")
@mcp.tool()
def list_oncall(team: str) -> dict:
"""Return the current on-call rotation for a team.
Args:
team: Team slug, e.g. platform or sre.
"""
rotations = {
"platform": {"primary": "alice", "secondary": "bob"},
"sre": {"primary": "carol", "secondary": "dave"},
}
if team not in rotations:
raise ToolError(f"Unknown team: {team}. Known teams: {', '.join(rotations)}")
return rotations[team]
@mcp.tool()
def ack_incident(incident_id: str, note: str) -> dict:
"""Acknowledge an incident.
Args:
incident_id: Incident identifier, e.g. INC-1234.
note: Acknowledgement note to record.
"""
if not incident_id.startswith("INC-"):
raise ToolError("incident_id must look like INC-1234")
logger.info("ack_incident called for %s", incident_id) # stderr, never stdout
return {"incident_id": incident_id, "acked": True, "note": note}
def main():
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
mcp.run(transport="streamable-http")
if __name__ == "__main__":
main()
Run it with uv run --with "mcp[cli]>=2.2.0" python server.py and it serves on 127.0.0.1:8000/mcp. Two things we confirmed that the docs tell you but experience makes you believe: stdio servers must never print to stdout (the SDK’s own quickstart warns this corrupts the JSON-RPC stream — log to stderr), and the run() signature takes transport="stdio" | "sse" | "streamable-http" with host, port, streamable_http_path, json_response, stateless_http, event_store, max_request_body_size, and transport_security kwargs on the HTTP path.
The client side is equally short. A URL means Streamable HTTP; mode="auto" probes server/discover and falls back to the legacy handshake for older servers:
import asyncio
import json
from mcp import Client
async def main() -> None:
async with Client("http://127.0.0.1:8000/mcp") as client:
tools = await client.list_tools()
print("TOOLS:", [t.name for t in tools.tools])
result = await client.call_tool("list_oncall", {"team": "platform"})
print("ONCALL:", result.content[0].text)
err = await client.call_tool("list_oncall", {"team": "nope"})
print("ERROR-PATH isError:", err.is_error, "|", err.content[0].text[:120])
asyncio.run(main())
TOOLS: [
"list_oncall",
"ack_incident"
]
ONCALL: {
"primary": "alice",
"secondary": "bob"
}
ERROR-PATH isError: True | Error executing tool list_oncall: Unknown team: nope. Known teams: platform, sre
The client worked first-try against the stateless server because the SDK handles the new headers (Mcp-Method, Mcp-Name, MCP-Protocol-Version) and per-request _meta automatically. The failure we hit was our own: the result attribute is is_error (snake_case), not isError — the SDK speaks Python, the wire speaks camelCase. Your hand-rolled client will not have that convenience; use the SDK.
Deploy It: Kubernetes Manifests for a Stateless Fleet
Stateless requests collapse the deployment story. No session affinity, no drain rituals, no sticky-session config on your ingress. One Deployment, one Service, one Ingress with the one non-obvious requirement the spec mandates: Origin header validation to prevent DNS-rebinding attacks against local servers, and authentication on every connection.
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-incident-ops
labels:
app: mcp-incident-ops
protocol: mcp-2026-07-28
spec:
replicas: 3
selector:
matchLabels:
app: mcp-incident-ops
template:
metadata:
labels:
app: mcp-incident-ops
protocol: mcp-2026-07-28
annotations:
# OTel context flows via _meta keys; export via OTLP
instrumentation.opentelemetry.io/inject-python: "true"
spec:
serviceAccountName: mcp-incident-ops
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: server
image: registry.internal/platform/mcp-incident-ops:2.2.0
args: ["server.py"]
env:
- name: MCP_TRANSPORT
value: streamable-http
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector.observability:4318
ports:
- name: http
containerPort: 8000
readinessProbe:
# server/discover is the health check for the modern era:
# self-contained POST, no session state required
httpGet:
path: /mcp
port: http
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /mcp
port: http
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "2"
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: mcp-incident-ops
spec:
selector:
app: mcp-incident-ops
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mcp-incident-ops
annotations:
# Any replica can serve any request — no session affinity anywhere
nginx.ingress.kubernetes.io/enable-session-affinity: "false"
nginx.ingress.kubernetes.io/proxy-read-timeout: "360"
# Long-lived subscription streams need generous read timeouts
spec:
rules:
- host: mcp-incident-ops.internal.example.com
http:
paths:
- path: /mcp
pathType: Prefix
backend:
service:
name: mcp-incident-ops
port:
number: 80
Two deliberate choices in those manifests. The probes hit /mcp without the modern headers, so they land on the legacy transport path and get its 400 — which is still proof the process is alive and routing; if you want a spec-pure probe, send server/discover with the MCP-Protocol-Version header from a sidecar or use the SDK’s json_response mode against a dedicated health path. And proxy-read-timeout: 360 matters: subscriptions/listen is a long-lived POST-response SSE stream, and a proxy that idle-times-out at 60 seconds will silently kill change notifications for every connected host.
For multi-tenancy, per-request identity now travels as headers. The spec’s x-mcp-header mechanism (SEP-2243) lets tool parameters carry custom headers on the request — here is the pattern for a tenant-scoped call:
{
"jsonrpc": "2.0",
"id": "c4",
"method": "tools/call",
"params": {
"name": "list_oncall",
"arguments": { "team": "platform" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "host-gw", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {},
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
}
}
The traceparent key is not decoration — the changelog documents OpenTelemetry context propagation conventions for exactly these _meta keys, and the SDK’s OpenTelemetryMiddleware consumes them. With sessions gone, this is how a request stays traceable across host → ingress → replica → backing system.
Lock the Door: OAuth 2.1, PRM, and the Confused Deputy
The 2026 security story is the part most teams will skip and regret. MCP authorization follows OAuth 2.1 conventions, and the mechanics are now standard-web: an unauthenticated request gets 401 with a WWW-Authenticate header pointing at a Protected Resource Metadata (RFC 9728) document at /.well-known/oauth-protected-resource, the client fetches it, discovers the authorization server and scopes, and runs the code flow. On top of that, this revision hardens specifics: authorization servers SHOULD include iss per RFC 9207 and clients MUST validate it against the recorded issuer before redeeming codes; credentials are keyed by issuer identifier and MUST NOT be reused across authorization servers; Dynamic Client Registration (RFC 7591) is deprecated in favor of Client ID Metadata Documents.
For platform fleets, though, per-user consent is usually wrong. The Enterprise-Managed Authorization extension puts the corporate IdP in the decision seat: the client exchanges a corporate ID token for an Identity Assertion JWT Authorization Grant (ID-JAG), and the MCP authorization server validates the ID-JAG and issues the access token. Onboarding and offboarding become IdP policy, not a consent screen per server per employee.
And read the security best practices doc before you deploy a proxy-style server, because it documents a real confused-deputy attack against exactly the architecture most teams build first: an MCP proxy that talks to third-party APIs with a static client ID, allows dynamic client registration, and does not enforce per-client consent before forwarding. Under those conditions an attacker registers a malicious client with redirect_uri: attacker.com, gets the user’s browser to hit the proxy’s auth flow (consent cookie already set from a previous legitimate approval), and receives a code without the user ever seeing a fresh consent screen. Mitigations: no static client IDs to third-party AS without per-client consent, pin redirect URIs, and audit every registration. Tool poisoning and rug pulls (a server changing tool behavior after review) are the other two attack classes the doc names — the reason your registry and your audit trail matter.
Day-2 Operational Warnings
- Monitor
isError, not just HTTP status. We verified it live: a failed tool call returns200 OKwithisError: true. Your golden signals for an MCP fleet aretools/callrate,isErrorratio, p99 per-tool latency, andUnsupportedProtocolVersionError/HeaderMismatchcounts. A fleet can be 100% green on infra metrics while every call fails semantically. - Alert on era-mixing. Header-less traffic silently lands on the legacy session transport. If you see session-ID errors in a fleet you believe is stateless, someone (a hand-rolled client, an old SDK) is speaking 2025-11-25. Grep logs for “Missing session ID” — it is your era-migration smoke detector.
- Long-lived streams need proxy tuning.
subscriptions/listenis a single long POST-response SSE stream per subscribed notification type. Default proxy idle timeouts (60s in many setups) will kill it. We setproxy-read-timeout: 360above; verify yours, and load-test with connected subscribers before you trust it. - Cache list results or burn tokens.
tools/listresponses now carryttlMs/cacheScopehints (SEP-2549). Hosts that ignore them re-list every turn. At fleet scale the host-side progressive discovery pattern — catalog meta-tools, search, inject definitions on demand — is the difference between a 2,000-token and a 150,000-token preamble. The docs recommend switching when definitions exceed roughly 1–5% of the context window. - Deprecation cliff is scheduled, not hypothetical. Roots, Sampling, Logging, HTTP+SSE, and
includeContextvalues are all in the deprecated registry with earliest removal at the first revision on or after 2027-07-28. Inventory your servers’ capability usage now (server/discovergives you the capability map per server); anything advertising sampling support has a dated exit. - Multi Round-Trip Requests change your timeout budget. With MRTR (SEP-2322), a server can answer
resultType: "input_required"and wait for the client to retry withinputResponses— elicitation without server-initiated requests. The SDK caps rounds atinput_required_max_rounds=10by default. Set explicit per-tool deadlines that account for the round-trips, or a stuck elicitation becomes a stuck request. - stdio still corrupts on stdout. As old as MCP itself, still true in 2.2.0: one stray
print()in a stdio server breaks the JSON-RPC stream. Log to stderr, always. The SDK’s own quickstart code comments call this out explicitly. - The MCP Registry is in preview — treat it as metadata, not trust. The official registry (backed by Anthropic, GitHub, Microsoft, PulseMCP) standardizes
server.jsondiscovery data and namespaces via DNS verification, but it explicitly warns of breaking changes and data resets before GA. Pin your server sources; do not let hosts resolve tools dynamically from a preview registry.
Who Should Skip This
If your “agents” are a single chat UI calling one OpenAI endpoint, you do not need MCP infrastructure — the protocol’s value appears at N tools × M hosts, not at one. If you are all-in on a single vendor’s closed tool-calling format and never plan to swap hosts or servers, MCP’s portability pitch is theoretical for you. And if you were about to build an MCP proxy to third-party APIs with a static client ID and no per-client consent — stop; the spec’s own security doc explains how that architecture leaks authorization codes.
Verdict
Revision 2026-07-28 is the revision that made MCP deployable. Stateless requests, header-enforced request identity, per-request trace context, cacheable list responses, and a narrowed (Sampling/Roots/Logging out the door) core turn what was a desktop-app integration protocol into something a platform team can actually operate: horizontal scale, standard OAuth, standard tracing, boring HTTP. The migration cost is real — every hand-rolled client and every proxy that assumed header-less JSON-RPC needs the SEP-2243 headers, and every sampling-dependent server needs a new plan — but the direction is unambiguous. Build new servers on 2026-07-28 semantics, put OAuth 2.1 with PRM in front of them, watch isError not status codes, and inventory your deprecated-feature usage before the 2027 removal window opens.
The MCP GitHub organization holds the spec, SDKs (TypeScript, Python, C#, Go, Rust at Tier 1), the Inspector, and the reference servers; the registry preview and the MCP Apps extension for interactive UI are the pieces still in motion. Commercial support paths are forming around the ecosystem — Anthropic’s Claude platform being the largest host-side example — but the protocol itself is vendor-neutral by design, which is the entire point.
References & Further Reading
- MCP Specification 2026-07-28 — Key Changes (the canonical list of everything this guide covers)
- Streamable HTTP transport, revision 2026-07-28 (stateless POST semantics, Origin validation, protocol version header)
- server/discover RPC (mandatory discovery, version listing)
- Deprecated features registry (Roots, Sampling, Logging, HTTP+SSE, includeContext)
- MCP Security Best Practices (confused deputy, tool poisoning, rug pulls)
- MCP Authorization tutorial (OAuth 2.1, RFC 9728 PRM, RFC 9207 iss)
- Enterprise-Managed Authorization extension (ID-JAG flow)
- Client best practices: progressive tool discovery and programmatic tool calling
- modelcontextprotocol/python-sdk (v2.2.0 — MCPServer, Client, era-routing, OTel middleware)
- SEP-2575, SEP-2322, SEP-2243, SEP-2577, SEP-2549 (the spec-change PRs behind this revision)
- RFC 9728: OAuth Protected Resource Metadata · RFC 9207: iss in authorization responses