| id | mcp-2026-07-28-migration |
|---|---|
| type | guide |
| title | Migrate a Python MCP server to the 2026-07-28 specification |
| summary | Update a Python MCP server for the stateless 2026-07-28 specification by removing the initialize handshake, adopting the input_required round-trip, dropping deprecated features, and hardening authorization. |
| lang | en-US |
| content_version | 1 |
| status | reviewed |
| reviewed_on | 2026-09-06 |
The 2026-07-28 Model Context Protocol specification is the largest release since remote
MCP: it removes the initialize handshake entirely, replaces server-initiated requests
with a multi-round-trip input_required flow, and deprecates roots, sampling, and
logging. MCP is now stewarded by the Agentic AI Foundation under the Linux Foundation,
and the Python SDK shipped alongside the specification.
You are done migrating when your server answers tools/list and tools/call with no
session state of any kind, no tool depends on the deprecated server-initiated requests,
and your authorization path satisfies the new issuer rules.
- Stateless-first (SEP-2575, SEP-2567). The
initialize/initializedexchange and theMcp-Session-Idheader are retired. Every request self-describes with protocol version, client identity, and capabilities through_meta. An optionalserver/discoverRPC exists for clients that want capabilities up front. - Multi-round-trip replaces server-initiated requests (SEP-2322).
elicitation/create,sampling/createMessage, androots/listno longer hold open streams. A tool that needs client input returns a result withresultType: "input_required"plus the requests that need answers; the client retries the original call with the answers attached ininputResponses. - Per-request version and routing headers (SEP-2243).
MCP-Protocol-Versiontravels with every request (2026-07-28), and Streamable HTTP requests must includeMcp-MethodandMcp-Nameso infrastructure can route without parsing JSON bodies. - Authorization hardening. Authorization servers must return the
issparameter per RFC 9207 (SEP-2468), clients must setapplication_typeduring dynamic registration (SEP-837), and client credentials are issuer-bound — never reused across authorization servers (SEP-2352). - Response caching (SEP-2549). Responses from
tools/list,prompts/list,resources/list, andresources/readcarryttlMsandcacheScope. - Tasks restructured (SEP-2663). Tasks moved into the
io.modelcontextprotocol/tasksextension with poll-basedtasks/get, a newtasks/update, and change notifications moved to asubscriptions/listenstream. - Deprecations with a minimum twelve-month window. Roots, sampling, and logging are deprecated (SEP-2577), the legacy HTTP+SSE transport has a one-year offramp, and Dynamic Client Registration is replaced by Client ID Metadata Documents (CIMD).
- Delete the handshake. Remove
initializeandnotifications/initializedhandling and everyMcp-Session-Idlookup. A request that still sendsinitializeshould fail as an unknown method, not be negotiated. - Make each request self-sufficient. Anything your server previously remembered
from initialize — client capabilities, protocol version, identity — must now be read
per-request from
_meta, or fetched throughserver/discoverwhen a client opts in. - Replace elicitation with the input_required round-trip. A tool that used to call
elicitation/createnow returnsresultType: "input_required"with the questions, and completes when the client retries withinputResponses. - Stop building on deprecated primitives. Roots, sampling, and logging still work during the deprecation window, but new code should not depend on them.
- Upgrade the official Python SDK. The TypeScript, Python, Go, and C# SDKs shipped with the specification; the RC-to-final window was roughly ten weeks, so older SDK versions predate the breaking changes.
- Update transports. Drop the legacy HTTP+SSE transport on its one-year offramp and
emit the required
Mcp-MethodandMcp-Nameheaders on Streamable HTTP. - Check the authorization path. Validate
issper RFC 9207, setapplication_typeduring registration, and keep one credential per issuer. - Adopt caching metadata. Annotate list responses with
ttlMsandcacheScopeso stateless infrastructure can cache them safely.
The runnable reference lives in examples/mcp-server. The essential shape:
class InputRequired(Exception):
def __init__(self, requests):
super().__init__("tool execution needs additional client input")
self.requests = requests
def delete_resource(args, input_responses=None):
answer = str((input_responses or {}).get("confirm", "")).strip().lower()
if answer != "yes":
raise InputRequired([{"id": "confirm", "prompt": "Type 'yes' to confirm deletion."}])
return "deleted"The server catches InputRequired before its generic exception handler and returns:
{
"result": {
"resultType": "input_required",
"requests": [{"id": "confirm", "prompt": "Type 'yes' to confirm deletion."}],
"content": [{"type": "text", "text": "Additional client input is required before this tool can finish."}]
}
}The client retries the identical tools/call with
"inputResponses": {"confirm": "yes"}. Because the flow is retry-based, an unconfirmed
call never mutates anything — the same property that makes idempotent automation safe.
Verify the complete behavior, including JSON-RPC error codes and the removed handshake:
python examples/mcp-server/verify.py starter --expect-failure
python examples/mcp-server/verify.py solutionThis guide works from the published release notes and the shipped SDKs. Field-level wire
schemas for _meta, the requests entries, and cacheScope values live in the
specification and SDK types — consult them before shipping, and prefer the official
Python SDK over hand-rolled dispatch in production. Authorization deployment details
issuer discovery, credential storage, and CIMD rollout, are also out of scope here.