Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Apa yang berubah di spesifikasi MCP 2026-07-28?
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.
02Bagaimana mengganti Mcp-Session-Id di server MCP yang sudah ada?
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.
03Apa itu Multi Round-Trip Requests di MCP?
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.
04Apakah server MCP di belakang load balancer masih butuh sticky session?
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.
05Fitur MCP apa saja yang deprecated di 2026-07-28?
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.
Migrasi Spec MCP 2026-07-28: Panduan Stateless untuk Server
Revisi MCP 2026-07-28 menghapus handshake dan session ID. Apa yang rusak di server sessionful, dan perubahan TypeScript, MRTR serta load balancer penggantinya.
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.
Apa yang Berubah dalam Migrasi Spec MCP 2026-07-28
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.
Tanpa Handshake: Setiap Request Membawa _meta Sendiri
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 list
server/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.
Mengganti Mcp-Session-Id di Server TypeScript
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.
Menulis Ulang Elicitation sebagai Multi Round-Trip Requests
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.
Mcp-Method, Mcp-Name dan Load Balancer Tanpa Sticky Session
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.
List yang Bisa Di-cache: ttlMs dan cacheScope
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.
Deprecated: DCR, HTTP+SSE, Roots, Sampling dan 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.
Urutan Migrasi yang Menjaga Client Lama Tetap Jalan
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.
Pindah ke package v2 dan sajikan lewat createMcpHandler. Pertahankan default legacy: 'stateless' hanya bila server v1 Anda memang sudah stateless; kalau tidak, pasang isLegacyRequest di depan handler legacy: 'reject' dan biarkan jalur sessionful lama apa adanya.
Cari setiap pembacaan session id dan ganti: storage bersama plus handle eksplisit untuk state yang hidup lebih lama dari satu panggilan, requestState yang ditandatangani untuk state yang hanya mencakup satu alur multi-ronde. Muat signing key dari konfigurasi agar setiap replica bisa memverifikasi setiap retry.
Tulis ulang panggilan elicitInput dan requestSampling menjadi return inputRequired, cek capabilities yang dideklarasikan client sebelum bertanya, dan buat setiap side effect di balik konfirmasi menjadi idempotent.
Deklarasikan cacheHints untuk setiap operasi list dan read, private secara default, dan kembalikan tools dalam urutan tetap.
Hapus affinity di load balancer, jawab GET dan DELETE dengan 405, dan bila Anda mem-publish perubahan list lintas node, berikan ServerEventBus yang sama ke setiap createMcpHandler agar stream subscriptions/listen di node mana pun menerimanya.
Sisir kerusakan kecil: kode resource-not-found dari -32002 ke -32602, log yang bergantung pada logging/setLevel, dan kode client yang membaca resultType atau elicitationId yang sudah dihapus.
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.