Backend
Migrasi Spec MCP 2026-07-28: Panduan Stateless untuk Server
Oktober 202612 menit baca

Protokolnya menjadi stateless. Handshake initialize dan header Mcp-Session-Id dihapus, dan setiap request membawa versi protokol serta client capabilities di _meta. Request dari server ke client diganti Multi Round-Trip Requests, dan Streamable HTTP menambah header wajib Mcp-Method dan Mcp-Name serta cache hint di hasil list.
Pindahkan state yang harus bertahan lebih dari satu panggilan ke storage bersama, lalu berikan client handle eksplisit buatan server sebagai argumen tool biasa. Untuk state yang hanya mencakup satu interaksi multi-langkah, gunakan requestState yang dilindungi integritasnya dan dikirim balik oleh client. Dalam kedua cara, cek di setiap panggilan bahwa pemanggil memang berhak memakai handle tersebut.
Fitur ini menggantikan request dari server ke client seperti elicitation/create dan sampling/createMessage. Server mengembalikan result dengan resultType input_required dan map inputRequests, lalu client mengulang panggilan awal dengan request id baru, inputResponses yang cocok, dan requestState yang persis sama. Hanya tools/call, prompts/get dan resources/read yang boleh mengembalikan input_required.
Tidak untuk revisi 2026-07-28, karena setiap request berdiri sendiri dan tidak ada session di level protokol. Anda bisa memakai round-robin atau least-connections, dan melakukan routing berdasarkan header Mcp-Method dan Mcp-Name tanpa mem-parse body JSON. Server tetap wajib mencocokkan header itu dengan body dan menolak yang tidak cocok dengan error -32020.
Roots, Sampling dan Logging dinyatakan deprecated, begitu juga Dynamic Client Registration yang digantikan Client ID Metadata Documents, serta transport HTTP+SSE lama. Fitur deprecated tetap berfungsi minimal dua belas bulan sesuai lifecycle policy yang baru. Beberapa hal langsung dihapus, termasuk ping, logging/setLevel, endpoint stream GET dan resumption Last-Event-ID.

Ringkasan Utama
Spesifikasi MCP 2026-07-28 menghapus handshake initialize dan header Mcp-Session-Id, sehingga setiap request membawa versi dan capabilities-nya sendiri di _meta. Migrasi server berarti memindahkan state session ke handle eksplisit atau requestState yang ditandatangani, mengembalikan input_required alih-alih elicitation dari server, mengirim cache hint di hasil list, dan menghapus sticky session di load balancer.
Migrasi spec MCP 2026-07-28 adalah migrasi pertama yang menyentuh semua server, bukan hanya yang memakai fitur baru. Saya membaca changelog-nya baris demi baris dan mencocokkannya dengan bentuk server TypeScript sessionful yang umum: map transport dengan key Mcp-Session-Id, objek draft yang menempel pada id yang sama, panggilan elicitation yang menunggu di stream SSE yang terbuka, dan upstream nginx yang di-pin dengan ip_hash agar session tetap di node yang membuatnya. Keempatnya dihapus atau dirombak di revisi ini.
Panduan ini membahas setiap breaking change sesuai urutan Anda akan menemuinya, dengan kode sebelum dan sesudah untuk server TypeScript di package SDK v2 serta konfigurasi load balancer yang tidak lagi butuh affinity. Semua isinya diambil dari spesifikasi 2026-07-28, changelog-nya, posting pengumuman dari para lead maintainer, dan panduan migrasi resmi TypeScript SDK. Pengumuman itu menyebut TypeScript, Python, Go dan C# sebagai SDK Tier 1 yang mendukung revisi ini saat rilis, sementara Rust masih beta.
Intinya, MCP sekarang stateless di level protokol. SEP-2575 menghapus pertukaran initialize dan notifications/initialized, dan SEP-2567 menghapus session di level protokol beserta header Mcp-Session-Id. Semua hal lain di tabel ini mengikuti keputusan itu: begitu tidak ada request yang boleh bergantung pada request sebelumnya, request dari server ke client, notifikasi perubahan, dan resumption stream semuanya butuh bentuk baru.
| Area | 2025-11-25 | 2026-07-28 | Yang Anda ubah |
|---|---|---|---|
| Setup koneksi | initialize, lalu notifications/initialized | Tanpa handshake. Versi, client capabilities dan client info dikirim di _meta setiap request. Server wajib mengimplementasikan server/discover | Baca identitas per request, bukan per koneksi |
| Session | Header Mcp-Session-Id, DELETE untuk mengakhiri | Dihapus. Hasil list tidak lagi berbeda per koneksi | Ganti state yang di-key oleh session dengan handle eksplisit |
| Request server ke client | elicitation/create, sampling/createMessage, roots/list dikirim lewat stream SSE | Multi Round-Trip Requests: kembalikan resultType input_required, client mengulang dengan inputResponses | Tulis ulang panggilan blocking menjadi return-lalu-retry |
| Notifikasi perubahan | Stream HTTP GET, resources/subscribe | Satu stream POST subscriptions/listen yang dipilih client secara opt-in | Publish lewat event bus yang dipakai bersama semua node |
| Header HTTP | MCP-Protocol-Version | Tambahan Mcp-Method dan Mcp-Name yang wajib, Mcp-Param-* opsional lewat x-mcp-header | Validasi header terhadap body, lakukan routing berdasarkan header |
| Hasil list | Tanpa metadata cache | ttlMs dan cacheScope wajib di tools/list, prompts/list, resources/list, resources/read, resources/templates/list | Deklarasikan cache hint per operasi |
| Level log | RPC logging/setLevel | io.modelcontextprotocol/logLevel di _meta setiap request | Siap-siap log kosong bila client tidak mengirimnya |
| Resumption stream | Replay dengan Last-Event-ID | Dihapus. Stream yang putus menghilangkan request, dan client mengirim ulang dengan id baru | Hapus event store |
| Tasks | Eksperimental, di core | Extension io.modelcontextprotocol/tasks, di-poll dengan tasks/get, input lewat tasks/update | Implementasi ulang di atas extension bila Anda memakainya |
Dua aturan kecil sering terlewat. Setiap result sekarang membawa field resultType yang wajib, berisi complete atau input_required, dan client harus memperlakukan result dari server lama yang tidak menyertakannya sebagai complete. Selain itu, error code resource-not-found pindah dari -32002 ke -32602, kode Invalid Params standar JSON-RPC, sehingga pengecekan di sisi client yang masih memakai angka lama diam-diam berhenti cocok.
Di 2025-11-25, client menghabiskan dua pesan untuk initialize sebelum bisa memanggil tool, dan server membuat session id yang harus disertakan di setiap request berikutnya. Di 2026-07-28, request pertama boleh langsung tools/call. _meta-nya membawa io.modelcontextprotocol/protocolVersion dan io.modelcontextprotocol/clientCapabilities, dan sebaiknya juga io.modelcontextprotocol/clientInfo. Server sebaiknya menjawab dengan io.modelcontextprotocol/serverInfo di _meta setiap result. Sisi HTTP mencerminkan body: header MCP-Protocol-Version harus sama dengan versi di _meta, kalau tidak server menolak request tersebut.
# BEFORE (2025-11-25): two handshake messages, then every call
# leans on a session the server minted and must remember.
POST /mcp {"method":"initialize", ...}
<- 200 Mcp-Session-Id: 1868a90c-4c1e-4b6e-9a0f-2f1d1c0c9e11
POST /mcp {"method":"notifications/initialized"}
POST /mcp
Mcp-Session-Id: 1868a90c-4c1e-4b6e-9a0f-2f1d1c0c9e11
MCP-Protocol-Version: 2025-11-25
{"jsonrpc":"2.0","id":7,"method":"tools/call",
"params":{"name":"post_journal","arguments":{"draftId":"JD-2026-0412"}}}
# AFTER (2026-07-28): one self-contained POST. Any node can answer it.
POST /mcp
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: post_journal
{"jsonrpc":"2.0","id":7,"method":"tools/call",
"params":{"name":"post_journal","arguments":{"draftId":"JD-2026-0412"},
"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"finance-desk","version":"3.2.0"},
"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
# Header and body disagree (or a required header is missing):
# 400 Bad Request {"error":{"code":-32020,"message":"Header mismatch: ..."}}
# Version the server does not speak:
# 400 Bad Request UnsupportedProtocolVersionError (-32022) + supported listserver/discover adalah satu-satunya method baru yang wajib diimplementasikan semua server. Method ini mengiklankan versi yang didukung, capabilities dan identitas, dan client boleh memanggilnya lebih dulu untuk memilih versi di awal atau sebagai probe kompatibilitas mundur. Konsekuensi praktisnya bagi penulis server: pengecekan capability pindah dari setup koneksi ke dalam request handler. Kalau sebuah tool ingin bertanya ke user, tool itu harus melihat clientCapabilities di request tersebut, karena spec melarang mengirim input request yang tidak dideklarasikan didukung oleh client.
Pola SDK v1 untuk server Streamable HTTP yang sessionful adalah map dari session id ke transport, route yang menangani POST, GET dan DELETE, serta pengecekan initialize yang membuat transport. Di package v2, entry point untuk 2026-07-28 adalah createMcpHandler dari @modelcontextprotocol/server, yang membangun server baru dari factory Anda untuk setiap request dan tidak menyimpan apa pun di antaranya. Berikut server journal draft yang sama dalam dua bentuk.
// BEFORE: @modelcontextprotocol/sdk v1, sessionful Streamable HTTP.
import { randomUUID } from "node:crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
const sessions = new Map<string, StreamableHTTPServerTransport>();
// The real problem: business state keyed by a protocol session id,
// living in ONE process. A second replica cannot see it.
const draftsBySession = new Map<string, JournalDraft>();
const route = async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId && sessions.has(sessionId)) {
return sessions.get(sessionId)!.handleRequest(req, res, req.body);
}
if (!sessionId && isInitializeRequest(req.body)) {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => sessions.set(id, transport),
});
await buildServer().connect(transport);
return transport.handleRequest(req, res, req.body);
}
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Session ID required" }, id: null });
};
app.post("/mcp", route);
app.get("/mcp", route); // standalone SSE stream for server pushes
app.delete("/mcp", route); // session teardown
// AFTER: v2 packages, one factory, a fresh server per request.
import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { toNodeHandler } from "@modelcontextprotocol/node";
import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
import * as z from "zod/v4";
function buildServer(): McpServer {
const server = new McpServer(
{ name: "erp-journal", version: "2.0.0" },
{ capabilities: { tools: {} } },
);
// SEP-2567: cross-call state becomes an explicit, server-minted handle
// passed as an ordinary tool argument, stored where every node can read it.
server.registerTool(
"create_journal_draft",
{ description: "Open a journal draft", inputSchema: z.object({ memo: z.string() }) },
async ({ memo }, ctx) => {
const draftId = await drafts.insert({ memo, owner: ctx.http?.authInfo?.clientId });
return { content: [{ type: "text", text: "Draft " + draftId + " opened" }] };
},
);
server.registerTool(
"add_journal_line",
{
description: "Add a debit or credit line to a draft",
inputSchema: z.object({ draftId: z.string(), account: z.string(), amount: z.number() }),
},
async ({ draftId, account, amount }, ctx) => {
// The handle is client-visible: check ownership on EVERY call.
await drafts.assertOwner(draftId, ctx.http?.authInfo?.clientId);
await drafts.addLine(draftId, { account, amount });
return { content: [{ type: "text", text: "Line added to " + draftId }] };
},
);
return server;
}
// Default legacy: 'stateless' also answers 2025-era clients per request.
const handler = createMcpHandler(buildServer);
const app = createMcpExpressApp({ host: "0.0.0.0", allowedHosts: ["mcp.example.com"] });
const node = toNodeHandler(handler);
app.all("/mcp", (req, res) => void node(req, res, req.body));
app.listen(3000);Ada tiga detail penting. Pertama, v2 tidak mengaktifkan revisi baru secara tidak sengaja: panduan SDK menyatakan bahwa Server atau McpServer yang dibuat manual tetap memakai protokol era 2025 sampai Anda menyajikannya lewat createMcpHandler atau serveStdio. Kedua, opsi default legacy: 'stateless' melayani client era 2025 dari factory yang sama, tetapi hanya dengan pola stateless. Kalau server v1 Anda sessionful, jalur yang disarankan panduan adalah createMcpHandler ketat dengan legacy: 'reject', dengan isLegacyRequest yang meneruskan traffic lama ke handler yang sudah ada. Ketiga, draft id sekarang terlihat oleh client dan diketik oleh model, jadi kepemilikannya harus dicek di setiap panggilan, bukan diasumsikan dari session.
SEP-2322 menghapus seluruh kanal request dari server ke client. Tool yang butuh konfirmasi mengembalikan InputRequiredResult dengan resultType input_required, map inputRequests berisi request elicitation, sampling atau roots, dan requestState opsional yang opaque. Client mengumpulkan jawabannya lalu mengulang panggilan awal dengan JSON-RPC id baru, inputResponses dengan key yang sama, dan string requestState persis seperti aslinya. Hanya tools/call, prompts/get dan resources/read yang boleh mengembalikannya. Di TypeScript SDK ini adalah helper inputRequired, dibaca kembali dengan acceptedContent dan ctx.mcpReq.requestState.
// BEFORE (v1): the handler blocks on a server->client request that
// travels back over the open SSE stream of THIS process.
const answer = await server.server.elicitInput({
message: "Post draft " + draftId + " to the ledger?",
requestedSchema: {
type: "object",
properties: { confirm: { type: "boolean" } },
required: ["confirm"],
},
});
// AFTER (2026-07-28): return input_required and finish on the retry,
// which may land on a different node.
import {
acceptedContent, createRequestStateCodec, inputRequired, McpServer,
} from "@modelcontextprotocol/server";
import type { CallToolResult, InputRequiredResult } from "@modelcontextprotocol/server";
const CONFIRM = z.object({ confirm: z.boolean() });
type PostState = { step: "confirm"; draftId: string };
// HMAC-signed, NOT encrypted: the client can read it, but not forge it.
// The key must be shared by every replica, or a retry that lands on
// another node fails verification with -32602.
const stateCodec = createRequestStateCodec<PostState>({
key: Buffer.from(process.env.MCP_STATE_KEY!, "base64"),
ttlSeconds: 300,
});
const server = new McpServer(
{ name: "erp-journal", version: "2.0.0" },
{ capabilities: { tools: {} }, requestState: { verify: stateCodec.verify } },
);
server.registerTool(
"post_journal",
{ description: "Post a balanced draft to the ledger", inputSchema: z.object({ draftId: z.string() }) },
async ({ draftId }, ctx): Promise<CallToolResult | InputRequiredResult> => {
const state = ctx.mcpReq.requestState<PostState>(); // already verified
const answer = acceptedContent(ctx.mcpReq.inputResponses, "confirm", CONFIRM);
// Wrong: trusting answer alone. Bind the confirmation to THIS draft.
if (state?.draftId !== draftId || answer?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: "Post draft " + draftId + " to the ledger?",
requestedSchema: CONFIRM,
}),
},
requestState: await stateCodec.mint({ step: "confirm", draftId }),
});
}
// requestState is replayable inside its TTL. Single-use must be
// enforced here: posting an already-posted draft is a no-op.
const posted = await ledger.postOnce(draftId);
return { content: [{ type: "text", text: posted ? "Posted " + draftId : draftId + " was already posted" }] };
},
);Legacy shim di SDK memungkinkan Anda menulis handler sekali dengan gaya ini dan tetap melayani client era 2025, dengan mengirim request server ke client yang sungguhan lewat session yang hidup. Panduannya tegas soal batasannya: di HTTP legacy yang stateless tidak ada jalur balik, jadi shim turun menjadi penolakan capability yang bersih. Perhatikan juga bahwa inputResponses berlaku per ronde dan tidak pernah terakumulasi. Alur dengan dua pertanyaan harus membawa jawaban pertama di dalam requestState, sebagai discriminated union dari fase-fase, bukan berharap kedua jawaban muncul di retry terakhir.
requestState melewati client, jadi spec mewajibkan server memperlakukannya sebagai input yang dikendalikan penyerang dan melindungi integritasnya dengan HMAC atau AEAD setiap kali ia memengaruhi otorisasi atau logika bisnis. createRequestStateCodec menandatanganinya tetapi tidak mengenkripsi, jadi jangan pernah menaruh rahasia di dalamnya. Dan tanda tangan tidak membuatnya sekali pakai: selama TTL-nya, state yang sama bisa di-replay, sehingga posting ERP atau pembayaran harus dibuat idempotent di sisi server.
SEP-2243 mewajibkan Mcp-Method di setiap request Streamable HTTP dan Mcp-Name di tools/call, resources/read dan prompts/get, berisi params.name atau params.uri. Tujuannya, menurut spec, agar gateway, WAF dan load balancer bisa melakukan routing dan metering tanpa mem-parse JSON. Digabung dengan core yang stateless, upstream tidak lagi butuh affinity sama sekali, dan Anda bisa mengirim tool yang berat ke pool terpisah berdasarkan namanya.
# BEFORE: sessions live in one node's memory, so the client must be pinned.
# hash $http_mcp_session_id does NOT work: initialize carries no session
# header, so the session is minted on one node and later requests hash
# to another. Client IP was the usual workaround, and NAT broke it.
upstream mcp_nodes {
ip_hash;
server 10.0.0.11:3000;
server 10.0.0.12:3000;
}
# AFTER: every request is self-contained. Spread load however you like,
# and route on the mirrored headers without parsing JSON bodies.
upstream mcp_nodes {
least_conn;
server 10.0.0.11:3000;
server 10.0.0.12:3000;
server 10.0.0.13:3000;
}
upstream mcp_reporting {
server 10.0.0.21:3000; # same code, sized for long report tools
}
# Mcp-Name arrives as $http_mcp_name. A Base64-sentinel value
# (=?base64?...?=) will not match and falls through to the default.
map $http_mcp_name $mcp_pool {
default mcp_nodes;
run_aging_report mcp_reporting;
run_month_end_close mcp_reporting;
}
server {
listen 443 ssl;
server_name mcp.example.com;
location = /mcp {
# GET and DELETE are gone in 2026-07-28; the spec says answer 405.
if ($request_method != POST) { return 405; }
proxy_pass http://$mcp_pool;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_buffering off; # per-request SSE responses
proxy_read_timeout 300s; # subscriptions/listen stays open
}
}Aturan keamanan di balik routing berbasis header adalah node harus mencocokkan header dengan body. Header yang hilang atau tidak cocok menghasilkan 400 dengan JSON-RPC error -32020 HeaderMismatch, dan nama yang bukan ASCII biasa dikirim dalam bentuk sentinel =?base64?...?=, yang wajib di-decode server sebelum dibandingkan. Itulah alasan pool di atas menjalankan kode yang sama: header yang berbohong membuat request ditolak oleh node mana pun yang menerimanya, bukan menjalankan tool lain. Spec juga meminta intermediary yang menegakkan policy berdasarkan header ini untuk menolak request yang MCP-Protocol-Version-nya lebih tua dari era validasi header.
SEP-2549 menambahkan interface CacheableResult. tools/list, prompts/list, resources/list, resources/read dan resources/templates/list wajib mengembalikan ttlMs, petunjuk kesegaran dalam milidetik, dan cacheScope, berisi public atau private, yang memberi tahu intermediary bersama apakah response boleh di-cache. Changelog juga meminta server mengembalikan tools dalam urutan yang deterministik, yang membantu cache di client dan prompt cache di penyedia model. Di TypeScript SDK, policy ini diatur dengan ServerOptions.cacheHints.
const server = new McpServer(
{ name: "erp-journal", version: "2.0.0" },
{
capabilities: { tools: {}, resources: {} },
cacheHints: {
// Same tool catalogue for every caller: shared caches may keep it.
"tools/list": { ttlMs: 60_000, cacheScope: "public" },
// Chart of accounts filtered by the caller's company: never public.
"resources/read": { ttlMs: 5_000, cacheScope: "private" },
},
},
);
// With no hint the SDK emits ttlMs: 0 and cacheScope: "private",
// so nothing is ever served from cache. The client caps any ttlMs at
// 24 hours, and a list_changed notification still evicts early.Default SDK sengaja konservatif, ttlMs 0 dan private, jadi migrasi tidak mengubah apa pun sampai Anda opt-in. Tandai result sebagai public hanya bila isinya sama untuk semua pemanggil. Di server ERP, sebagian besar resource difilter berdasarkan perusahaan, role atau gudang, dan itu harus tetap private, karena hint public mengundang cache bersama memberikan chart of accounts milik satu tenant ke tenant lain.
Logging berubah diam-diam. logging/setLevel dihapus, dan server tidak boleh mengirim notifications/message untuk request yang _meta-nya tidak memuat io.modelcontextprotocol/logLevel. Panduan TypeScript mencatat bahwa Client bawaannya tidak menyertakan key itu secara default, sehingga output ctx.mcpReq.log() hilang setelah upgrade. Pindahkan log operasional ke stderr atau OpenTelemetry, yang juga menjadi saran dari deprecation Logging.
Revisi ini juga memperkenalkan feature lifecycle formal dengan status Active, Deprecated dan Removed, serta jendela deprecation minimal dua belas bulan, yang dicatat di registry fitur deprecated. Fitur deprecated tetap berfungsi selama jendela itu, tetapi implementasi baru sebaiknya tidak mengadopsinya.
| Fitur | Perubahan | Pengganti yang disarankan |
|---|---|---|
| Roots | Deprecated oleh SEP-2577 | Kirim direktori atau file sebagai parameter tool, URI resource atau konfigurasi server |
| Sampling | Deprecated oleh SEP-2577 | Panggil API penyedia LLM langsung dari server |
| Logging | Deprecated oleh SEP-2577 | stderr di stdio, OpenTelemetry di tempat lain |
| Dynamic Client Registration | Deprecated oleh PR 2858, dipertahankan untuk authorization server lama | Client ID Metadata Documents |
| Transport HTTP+SSE, 2024-11-05 | Diklasifikasi ulang sebagai Deprecated oleh SEP-2596 | Streamable HTTP |
| includeContext thisServer dan allServers | Diklasifikasi ulang sebagai Deprecated oleh SEP-2596 | Hilangkan field-nya atau pakai none |
Deprecated dan removed adalah dua daftar yang berbeda. ping, logging/setLevel, notifications/roots/list_changed, endpoint stream GET dan resumability Last-Event-ID langsung hilang dari 2026-07-28, tanpa jendela. Sisi otorisasi juga diperketat: client wajib memvalidasi parameter iss RFC 9207 sebelum menukarkan code, wajib menyimpan credential dengan key issuer dan tidak memakainya ulang di authorization server lain, serta wajib mengirim application_type saat Dynamic Client Registration.
Urutannya penting karena core yang stateless mencabut fondasi yang menopang perubahan lainnya. Dengan urutan ini, setiap langkah bisa dirilis sendiri-sendiri sementara client era 2025 tetap berfungsi.
Langkah satu dan dua menanggung sebagian besar risiko, dan keduanya juga yang paling cepat memberi hasil: begitu tidak ada yang tinggal di memori proses di antara request, server bisa di-scale dengan menambah replica, dan rolling deploy tidak lagi memutus percakapan yang kebetulan di-pin ke node yang sedang diganti.
Panduan SDK mencatat tidak ada entry serving in-memory untuk 2026-07-28, karena InMemoryTransport hanya menghubungkan instance era 2025. Uji jalur modern dengan memberi StreamableHTTPClientTransport sebuah fungsi fetch yang langsung memanggil handler.fetch. URL-nya tidak pernah benar-benar dihubungi, jadi validasi header, retry MRTR dan cache hint semuanya berjalan in-process di dalam unit test.
Aturan yang saya ambil dari revisi ini sederhana: kalau sebuah nilai harus bertahan di antara dua request MCP, tempatnya di database Anda atau di requestState yang ditandatangani, tidak pernah di transport. Server yang sudah dibangun dengan cara itu bisa dimigrasi dalam satu sore. Server yang menggantungkan state bisnis pada Mcp-Session-Id menghadapi refactor sungguhan, dan jendela deprecation dua belas bulan adalah waktu untuk mengerjakannya, bukan alasan untuk menunda.