Kalau MCP server-mu punya tool yang meminjam model milik client untuk meringkas, mengklasifikasi, atau menulis ulang teks, tool itu langsung rusak begitu client memakai MCP 2026-07-28. Gagalnya mendadak: ctx.session.create_message() melempar NoBackChannelError, dan pesan error-nya tidak menyebut apa penggantinya. Artikel ini bahas apa yang dihapus dari protokol, dua pengganti yang didukung, kode yang bisa langsung dipakai, dan checklist singkat untuk memilih.
Yang perlu disiapkan
- Python 3.10 atau lebih baru
pip install "mcp>=1.27,<2"untuk jalur v1 yang stabil, atau jalur v2 kalau kamu siap ikut SDK terbaru (v2 GA sejak Agustus 2026)fastmcp>=4kalau server-mu pakai FastMCP- API key Anthropic atau OpenAI untuk jalur panggil langsung
- Satu MCP tool yang sekarang memakai sampling, biar ada yang benar-benar dimigrasikan
Apa yang berubah
Revisi 2026-07-28 bikin MCP stateless di level protokol. Ada dua akibat untuk sampling.
Pertama, server tidak bisa lagi mendorong request ke client sambil memproses request dari client. SEP-2322 mengganti server-initiated call (roots/list, sampling/createMessage, elicitation/create) dengan Multi Round-Trip Request. Server balas dengan InputRequiredResult yang membawa resultType: "input_required", map inputRequests, dan token requestState yang opaque. Client mengumpulkan jawabannya, lalu memanggil tool yang sama lagi dengan inputResponses plus token tadi.
Kedua, sampling di-deprecate sebagai fitur (SEP-2577), bareng roots dan logging. Saran migrasi di changelog-nya lugas: "integrate directly with LLM provider APIs instead of Sampling."
Keduanya tidak menghapus kemampuan bertanya ke model milik client. Yang dihapus itu kanal push-nya, bukan pertanyaannya. Kamu tetap bisa bertanya ke model pemanggil, cuma sekarang tool-nya harus balik dulu dan biarkan client datang lagi.
Langkah 1: cek dulu kamu di revisi mana
Pastikan client-mu negosiasi versi apa sebelum ubah kode:
curl -si http://localhost:8000/mcp \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}' | head -20
server/discover itu wajib di 2026-07-28 dan mengembalikan versi protokol, capability, serta identitas yang server-mu klaim. Kalau client tidak kirim header MCP-Protocol-Version, berarti kamu masih di jalur era handshake dan create_message masih jalan di sana.
Tiga gejala yang berarti sudah waktunya migrasi:
NoBackChannelErrordarictx.session.create_message()atauctx.elicit()MCPDeprecationWarningdi panggilanctx.info(...)pertamaAttributeError: 'Context' object has no attribute 'sample'di FastMCP 4, yang menghapussample()dansample_step()untuk semua era protokol
Jalur A: panggil provider langsung
Ini migrasi yang disarankan spesifikasi, dan untuk sebagian besar server kodenya malah lebih sedikit dari panggilan sampling yang digantikan. Simpan key di environment server, bikin client sekali di level modul, lalu generate di dalam tool.
import anthropic
from fastmcp import FastMCP
mcp = FastMCP("Summarizer")
llm = anthropic.AsyncAnthropic() # level modul, biar koneksi dipakai ulang
@mcp.tool
async def summarize(text: str) -> str:
"""Summarize a document in two sentences."""
response = await llm.messages.create(
model="claude-sonnet-4-5",
max_tokens=512,
system="Summarize the user's text in exactly two sentences.",
messages=[{"role": "user", "content": text}],
)
return response.content[0].text
Tool-nya sekarang jalan sama untuk semua client, termasuk yang tidak pernah mengimplementasikan capability sampling. Kamu yang pilih model, kamu bisa baca token usage, dan retry, timeout, caching, serta hitung biaya jadi kode aplikasi biasa, bukan negosiasi protokol. Tool juga bisa dites tanpa client, hal yang tidak mungkin di versi sampling.
Jalur B: pinjam model pemanggil lewat round trip
Kadang yang penting justru model pemanggilnya. Client enterprise bisa mewajibkan generasi lewat gateway mereka sendiri, dengan credential dan tagihan mereka, atau memang menolak menyerahkan API key ke server-mu. Untuk kasus itu, kamu simpan pertanyaannya dan buang push-nya.
Balikin InputRequiredResult di pass pertama, lalu baca jawabannya dari ctx.input_responses saat retry:
from fastmcp import Context, FastMCP
from mcp.types import (
CreateMessageRequest,
CreateMessageRequestParams,
CreateMessageResult,
InputRequiredResult,
SamplingMessage,
TextContent,
)
mcp = FastMCP("Research")
@mcp.tool
async def ask_the_caller(question: str, ctx: Context) -> str | InputRequiredResult:
"""Put a question to the caller's model and report what it answered."""
responses = ctx.input_responses
if responses is None:
return InputRequiredResult(
result_type="input_required",
input_requests={
"answer": CreateMessageRequest(
method="sampling/createMessage",
params=CreateMessageRequestParams(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=question),
)
],
max_tokens=100,
),
)
},
)
answer = responses["answer"]
if isinstance(answer, CreateMessageResult) and isinstance(answer.content, TextContent):
return answer.content.text
return "The client returned no completion."
Ada dua batasan di jalur ini. Client harus negosiasi 2026-07-28, karena client lama tidak punya cara mengembalikan input untuk panggilan yang masih jalan. Dan tiap pertanyaan memakan satu round trip penuh, selama itu tool-nya nge-block. Kalau perlu bawa nilai melintasi beberapa putaran, taruh di requestState dan kembalikan saat retry, karena instance server mana pun bisa menangani request kedua.
Di Python SDK v2 resmi, kamu jarang menulis loop itu manual. Deklarasikan dependency pakai Resolve, biar SDK client yang menjalankan round trip-nya.
Jalur C: tetap di v1 sambil client-mu menyusul
Kebijakan feature lifecycle memberi tiap fitur yang di-deprecate waktu minimal dua belas bulan sebelum boleh dihapus, jadi sampling/createMessage masih jalan untuk client yang bicara 2025-11-25 atau lebih lama. Pin jalur stabilnya dan anggap ini tenggat, bukan tujuan:
pip install "mcp>=1.27,<2"
Pasang batas atas itu dari sekarang kalau kamu publish package yang bergantung ke mcp. Kalau tidak, instalasi berikutnya akan menarik v2 dan bikin penggunamu kaget.
Memilih jalur
| Situasi | Pakai |
|---|---|
| Server punya key sendiri dan tool harus sama untuk semua client | Jalur A |
| Client mewajibkan generasi di gateway, credential, atau tagihannya sendiri | Jalur B |
| Client stdio lama di koneksi pra-2026 | Jalur C, dengan tanggal migrasi |
| Tool tidak butuh generasi sama sekali | Hapus panggilan sampling |
| Kamu mau token usage, retry, dan caching di bawah kontrol sendiri | Jalur A |
Error yang bakal kamu temui
NoBackChannelError artinya koneksinya 2026-07-28 dan tidak ada kanal untuk server-initiated request. Pindah ke Jalur A atau B. MCPDeprecationWarning di panggilan logging itu deprecation yang sama yang kena ctx.info(); tulis ke stderr di stdio atau pakai OpenTelemetry. Kalau tool MRTR tidak pernah dapat jawaban, cek dulu revisi yang dinegosiasi, lalu pastikan client-nya tahu cara memenuhi input request sampling/createMessage.
Kesimpulan
Sampling sudah deprecated, dan kanal tempat dia berjalan sudah dihapus. Dua pengganti menutupi pekerjaannya: panggil provider langsung kalau server-mu punya key sendiri (Jalur A, default), atau balikin InputRequiredResult kalau meminjam model pemanggil memang syaratnya (Jalur B). Kalau masih jalan di v1 untuk client lama, pin mcp>=1.27,<2 dan kasih tanggal untuk migrasi.
Langkah lanjut: grep -rn "create_message\|ctx.sample(\|ctx.elicit(" your-servers/ lalu putuskan per tool. Untuk yang masuk Jalur A, tambahkan test yang memanggil tool tanpa client, karena itu justru mode kegagalan yang dulu bisa lolos tanpa ketahuan.
Referensi
- Model Context Protocol, specification changelog 2026-07-28: https://modelcontextprotocol.io/specification/2026-07-28/changelog
- Model Context Protocol, Sampling (client feature): https://modelcontextprotocol.io/specification/2025-06-18/client/sampling
- FastMCP, Sampling guide: https://gofastmcp.com/servers/sampling
- Pydantic, Try the MCP Python SDK v2 beta: https://pydantic.dev/articles/mcp-python-sdk-v2-beta