Eski nesil istemcilere hizmet verme
Makine çevirisi
Bu sayfa İngilizce dokümantasyondan otomatik olarak çevrildi; esas alınması gereken sürüm İngilizce sayfadır. Yanlış görünen bir şey varsa, nasıl bildireceğinizi Çeviriler sayfası açıklar.
MCP'nin iki protokol nesli var: 2025-11-25 spesifikasyon sürümüne kadar uzanan initialize el sıkışması nesli ve modern nesil olan 2026-07-28. Bu ayrımın kendisini anlatan sayfa Protokol sürümleri.
Bu sayfa o ayrımın sunucu tarafını ele alır ve yanıt tek bir cümleye sığar: zaten dağıttığınız streamable_http_app() her ikisine de hizmet verir.
SDK her isteği MCP-Protocol-Version başlığına göre yönlendirir. 2026-07-28 belirten bir istek modern işleyiciye gider. El sıkışması neslinden bir sürüm belirten ya da hiç başlık taşımayan bir istek (2026 öncesi bir istemcinin initialize isteği tam da böyle gelir), o istemcilerin beklediği aktarıma gider: initialize el sıkışması, oturumlar, hepsi. Bu, istek başına, kodunuzdan önce ve o tek uygulama üzerinde olur.
Yani eski nesil istemci, ona göre bir şey inşa ettiğiniz bir hedef değil. Zaten yazdığınız sunucuya bağlanan bir şey. Hiçbir şey yapılandırmazsınız.
Note
Kelimenin tam anlamıyla hiçbir şey. legacy= diye bir seçenek yok, sürüm izin listesi yok,
bir nesli reddetmenin ya da devre dışı bırakmanın yolu yok: ne streamable_http_app()
üzerinde, ne run() üzerinde, ne de oturum yöneticisinde. İki nesil de her zaman açık. O
imzada nesle özgü bir anahtara en yakın şey stateless_http, ve bu sayfanın büyük kısmı da
ondan ibaret.
Tek işleyici, iki nesil
İşte kullanıcıya bir şey sorması gereken bir araç ve onu çağıran her iki nesilden istemci:
from typing import Annotated
from pydantic import BaseModel
from mcp import Client
from mcp.client import ClientRequestContext
from mcp.server import MCPServer
from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve
from mcp.types import ElicitRequestParams, ElicitResult
mcp = MCPServer("Bookshop")
class Quantity(BaseModel):
copies: int
async def ask_quantity() -> Elicit[Quantity]:
"""Resolver: ask the user how many copies to put aside."""
return Elicit("How many copies?", Quantity)
@mcp.tool()
async def reserve(title: str, quantity: Annotated[ElicitationResult[Quantity], Resolve(ask_quantity)]) -> str:
"""Reserve copies of a book, asking the user how many."""
if isinstance(quantity, AcceptedElicitation):
return f"Reserved {quantity.data.copies} of {title!r}."
return "Nothing reserved."
async def answer(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
return ElicitResult(action="accept", content={"copies": 2})
async def main() -> None:
async with (
Client(mcp, mode="legacy", elicitation_callback=answer) as legacy,
Client(mcp, elicitation_callback=answer) as modern,
):
for client in (legacy, modern):
result = await client.call_tool("reserve", {"title": "Dune"})
print(client.protocol_version, result.structured_content)
reserve, modelin sağlamadığı tek bir şeye ihtiyaç duyar: kaç kopya. Bir araç bunu Annotated[..., Resolve(ask_quantity)] ile bildirir (ayrıntıların tamamı Bağımlılıklar sayfasında). reserve içinde hiçbir şey bir sürüm adı vermez, bir yetenek kontrol etmez ya da dallanmaz.
İki istemci aynı anda, aynı mcp nesnesi üzerinde açıktır. mode="legacy", initialize el sıkışmasını çalıştırır: 2026 öncesi bir istemcinin açtığı bağlantının ta kendisi. Diğeri varsayılanı alır ve 2026-07-28 sürümünde karar kılar.
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
2026-07-28 {'result': "Reserved 2 of 'Dune'."}
Aynı sunucu, aynı işleyici, aynı yanıt. Özelliğin tamamı bu.
Nasıl olduğu üzerinde durmaya değer, çünkü iki istemciye aynı soru bambaşka iki yoldan soruldu. 2026-07-28 bağlantısında sunucunun istek gönderebileceği bir kanal yoktur; bu yüzden Resolve soruyu araç sonucunun içinde döndürdü ve istemci çağrıyı yanıtla birlikte yeniden denedi (Çok turlu istekler (multi-round-trip)). 2025-11-25 bağlantısında böyle bir şey yoktur; orada Resolve, çağrının ortasında canlı bir elicitation/create isteği gönderdi ve bekledi. İkisini de siz yazmadınız. Resolve bağlantının anlaşılan sürümünü okur ve seçer; araç gövdeniz her iki durumda da bir AcceptedElicitation görür.
Tip
Nesiller arası bu taşınabilirlik, Resolve'un üzerine inşa edilecek API olmasının
nedenidir. Eski kardeşi ctx.elicit()
(Elicitation (kullanıcıdan bilgi isteme)) yalnızca
elicitation/create gönderir; dolayısıyla yalnızca eski nesil bir bağlantıda çalışır.
2026-07-28 bağlantısında çağrı başarısız olur. Bir araç hâlâ onu kullanıyorsa çözüm bir
sürüm kontrolü değil, yukarıda gördüğünüzdür.
Eski nesil bir oturumun size maliyeti
Yönlendirme bedava. Oturum değil.
2026-07-28 bağlantısı oturumsuzdur: her istek tek başına durur ve modern işleyici asla Mcp-Session-Id vermez. Eski nesil bağlantı bunun tam tersidir. 2026 öncesi bir istemci initialize gönderdiği anda SDK bir Mcp-Session-Id üretir, onu bir yanıt başlığında döndürür ve istemcinin sonraki isteklerinin bulabilmesi için arkasında canlı bir kayıt tutar: anlaşılan sürüm, açık akışlar, oturumu yürüten bir arka plan görevi.
Bu kayıt süreç içi, düz bir dict'tir. Dağıtık bir oturum deposu yoktur ve bir tane takmanın yolu da yoktur.
Tek worker'da bu görünmez. İki worker'da ise sorunun tamamı budur: Mcp-Session-Id taşıyan ve onu üretmemiş bir worker'a düşen bir istek o dict'te hiçbir şey bulamaz ve yanıt araç sonucu değil, bir 404 (Session not found) olur. Yani birden fazla worker çalıştırdığınız anda eski nesil istemciler yapışkan yönlendirmeye (sticky routing) ihtiyaç duyar: bir oturumdaki her istek, onu başlatan sürece ulaşmak zorundadır. Modern istemcilerin buna hiç ihtiyacı olmaz; yapışacakları bir oturumları yoktur. Yapışkanlığı ve bunlardan birden fazlasını çalıştırmaya dair geri kalan her şeyi Dağıtım ve ölçekleme sayfası ele alır.
Warning
event_store= çözüm gibi görünür ama değildir. O bir oturum deposu değil,
devam ettirilebilirliktir (kaçırılan SSE olaylarını aynı oturuma yeniden bağlanan bir
istemciye yeniden oynatmak). Bir oturumu asla başka bir süreçten erişilebilir kılmaz.
Tek ayar düğmesi: stateless_http
Yapışkanlık ödemeyi reddettiğiniz bir bedelse, değiştirebileceğiniz tam olarak tek bir şey var.
from typing import Annotated
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve
mcp = MCPServer("Bookshop")
class Quantity(BaseModel):
copies: int
async def ask_quantity() -> Elicit[Quantity]:
"""Resolver: ask the user how many copies to put aside."""
return Elicit("How many copies?", Quantity)
@mcp.tool()
async def reserve(title: str, quantity: Annotated[ElicitationResult[Quantity], Resolve(ask_quantity)]) -> str:
"""Reserve copies of a book, asking the user how many."""
if isinstance(quantity, AcceptedElicitation):
return f"Reserved {quantity.data.copies} of {title!r}."
return "Nothing reserved."
app = mcp.streamable_http_app(stateless_http=True)
Bu, sayfanın başındaki sunucuya tek bir anahtar sözcük eklenmiş hali. stateless_http=True, eski nesil kolun bunun yerine istek başına, kullan-at bir oturum kurmasını sağlar: Mcp-Session-Id verilmez, istekler arasında hiçbir şey hatırlanmaz; böylece herhangi bir worker herhangi bir isteğe hizmet verebilir ve yük dengeleyici canı ne isterse onu yapabilir.
Onunla ilgili iki şey, ne yaptığından daha önemli.
Yalnızca eski nesil kola dokunur. İstekler, stateless_http okunmadan önce sürüm başlığına göre yönlendirilir; bu yüzden modern yol onu hiç görmez. 2026-07-28 bağlantısı zaten oturumsuzdur ve her iki değerde de tıpatıp aynıdır.
O kolda sunucudan istemciye giden her iki kanala da mal olur. Tek bir POST boyunca yaşayan bir oturumun, sunucunun istek itebileceği bir akışı da bildirim itebileceği bağımsız bir akışı da yoktur. Sunucunun başlattığı her istek NoBackChannelError fırlatır: ctx.elicit(), emekliye ayrılmış örnekleme (sampling) ve kök dizinler (roots) çağrıları (Kullanım dışı özellikler) ve evet, eski nesil bir istemciye sorusunu soran Resolve da. Bildirimler bir hata bile almaz; sessizce düşürülür.
Note
json_response=True o düğme değildir ama aynı bedelin yarısını her eski nesil oturumda
öder: tek bir JSON gövdesiyle yanıtlanan bir POST'un istek kapsamlı kanal için akışı
yoktur; bu yüzden istek ortasındaki bir ctx.elicit() aynı NoBackChannelError istisnasını
fırlatır ve istekle ilişkili bildirimler düşürülür. Oturumun bağımsız akışına dokunulmaz:
ilgisiz bildirimler gelmeye devam eder.
Check
Yanlış olanı yapın. reserve, az önce iki istemciye de hizmet veren aracın ta kendisi. Onu
stateless_http=True ile dağıtın, aynı iki istemciyi HTTP üzerinden bağlayın ve her birinden
çağırın.
Modern istemci hâlâ Reserved 2 of 'Dune'. alır. Modern kol değişmedi.
Eski nesil istemcinin çağrısı, modelin okuyabileceği bir is_error sonucu olarak geri
dönmez. İsteğin tamamı, üst düzey bir protokol hatası olarak başarısız olur:
mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
Resolve sizi kurtarmadı. 2025-11-25 bağlantısında elicitation/create göndermek
zorundadır ve ihtiyaç duyduğu kanal, stateless_http=True'nun elden çıkardığı şeyin ta
kendisidir. Nesiller arası taşınabilir kod, geri kanala (back-channel) ihtiyaç duymayan kod
demek değildir.
Yani bu gerçek bir ödünleşmedir ve yalnızca eski nesil kolda vardır: oturumlu ve yapışkan, ya da durumsuz ve tek yönlü. Araçlarınız hiçbir zaman istemciye geri çağrı yapmıyorsa stateless_http=True bedavadır ve almalısınız. Yapıyorlarsa oturumları koruyun ve yönlendirmeyi yapışkan tutun.
Kodunuzun gerçekten çatallandığı yer
Neredeyse hiçbir yerde.
Araçlar, kaynaklar, prompt'lar, yapılandırılmış çıktı, ilerleme, hatalar: hiçbiri hangi neslin çağırdığını umursamaz. initialize el sıkışması, Mcp-Session-Id, bağımsız akış, bir oturumu bitiren DELETE: hepsinin sahibi SDK'dır ve bir işleyici bunların hiçbirini görmez. Etkileşimli girdi, nesillerin iletilen veride gerçekten ayrıştığı tek yerdir ve Resolve bunun sizin sorununuz olmaması için vardır: az önce tek bir aracın ikisine de hizmet verdiğini izlediniz.
Geriye tam olarak tek bir şey kalıyor, o da değişiklik bildirimleri; çünkü iki nesil farklı borulardan dinler:
2026-07-28istemcisi birsubscriptions/listenakışı açar ve abonelik veri yolunu okur.ctx.notify_resource_updated()(venotify_tools_changed(),notify_prompts_changed(),notify_resources_changed()) oraya, ve yalnızca oraya yayımlar. Bunun sayfası Abonelikler.- Eski nesil bir istemci, oturumunun açık tuttuğu bağımsız akışı okur.
ctx.session.send_resource_updated()(vesend_tool_list_changed()ile benzerleri) isteği taşıyan bağlantıya yazar: eski nesil bir oturum için bu, onun bağımsız akışıdır. Modern bir bağlantıda bunun yeri yoktur: HTTP üzerinde böyle bir kanal yoktur, stdio üzerinde ise dört değişiklik bildirimi türü yalnızcasubscriptions/listenakışlarında taşınır; bu yüzden modern bir bağlantıda bildirim sessizce düşürülür.
HTTP üzerinde iki çağrı da diğer neslin istemcilerine ulaşmaz. Herkese haber vermek için ikisini de çağırın:
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Bookshop")
STOCK = {"Dune": 3}
@mcp.resource("stock://{title}")
def stock(title: str) -> str:
"""How many copies of one book are on the shelf."""
return f"{STOCK[title]} in stock"
@mcp.tool()
async def restock(title: str, copies: int, ctx: Context) -> str:
"""Put copies of a book back on the shelf."""
STOCK[title] = STOCK.get(title, 0) + copies
await ctx.notify_resource_updated(f"stock://{title}")
await ctx.session.send_resource_updated(f"stock://{title}")
return f"{STOCK[title]} in stock"
İki satır, if yok, sürüm kontrolü yok, ve işiniz bitti. Eski nesil bir istemci var diye bir işleyicinin farklı yaptığı şeylerin listesinin tamamı bu.
Özet
- Tek bir
streamable_http_app()iki protokol nesline de hizmet verir. SDK her isteğiMCP-Protocol-Versionbaşlığına göre yönlendirir; yapılandırılacak bir şey ve aranacak bir nesil düğmesi yoktur. - Eski nesil bir istemci size bir oturuma mal olur: arkasında dağıtık bir depo olmayan, süreç içi bir
Mcp-Session-Idkaydı. Birden fazla worker yapışkan yönlendirme demektir; aksi halde yanlış worker404 Session not foundyanıtını verir. Çoklu worker'a dair ayrıntıların tamamı Dağıtım ve ölçekleme sayfasında. - Tek düğme
stateless_http=True'dur ve yalnızca eski nesil kolu etkiler. Eski nesil istemciler için bedava yük dengelemeyi, o koldaki sunucudan istemciye giden her iki kanal pahasına satın alır: sunucunun başlattığı isteklerNoBackChannelErrorfırlatır (istemcideis_errorsonucu değil, üst düzey bir hata) ve bildirimler düşürülür. 2026-07-28bağlantısı her durumda oturumsuzdur.stateless_httpona hiç dokunmaz.- İşleyici kodunuz nesle göre tam olarak tek bir yerde çatallanır: değişiklik bildirimleri.
ctx.notify_*subscriptions/listenistemcilerine ulaşır;ctx.session.send_*eski nesil oturumlara ulaşır. İkisini de çağırın. - Geri kalan her şey (
Resolvearacılığıyla kullanıcıdan girdi istemek dahil) tasarımı gereği nesiller arası taşınabilirdir. Modern olanı bir kez yazın.