Ana içeriğe geç

İstemci callback'leri

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'de neredeyse her istek tek yöne gider: istemciden sunucuya.

Sunucu da istemciden bir şeyler isteyebilir: kullanıcıya soru sormasını, kullanıcının modelinden örnekleme yapmasını, kullanıcının çalışma alanı klasörlerini listelemesini. Bu istekleri Client(...)'a callback'ler (geri çağırma işlevleri) geçirerek yanıtlarsınız.

Soru soran bir sunucu

İşte aracı kendi başına tamamlanamayan bir sunucu:

server.py
from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Context

mcp = MCPServer("Library")


class CardHolder(BaseModel):
    name: str


@mcp.tool()
async def issue_card(ctx: Context) -> str:
    """Issue a new library card."""
    answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
    if answer.action == "accept":
        return f"Card issued to {answer.data.name}."
    return "No card issued."
  • ctx.elicit(...), istemciye bir elicitation/create isteği gönderir ve bekler.
  • Araç, birisi (form dolduran bir kişi ya da kodunuz) bir name sağlayana kadar dönmez.

Bu, işin sunucu tarafı; ona Elicitation sayfası bakar. Bu sayfa ise hattın öteki ucu.

Elicitation callback'i

client.py
from mcp import Client
from mcp.client import ClientRequestContext
from mcp.types import ElicitRequestParams, ElicitResult


async def handle_elicitation(
    context: ClientRequestContext,
    params: ElicitRequestParams,
) -> ElicitResult:
    return ElicitResult(action="accept", content={"name": "Ada Lovelace"})


async def main() -> None:
    async with Client(
        "http://127.0.0.1:8000/mcp",
        mode="legacy",
        elicitation_callback=handle_elicitation,
    ) as client:
        result = await client.call_tool("issue_card")
        print(result.content)
  • Bir elicitation (kullanıcıdan bilgi isteme) callback'i async (context, params) -> ElicitResult biçimindedir.
  • params.message sorudur. params.requested_schema, sunucunun beklediği yanıtın JSON Schema'sıdır. Gerçek bir istemci bundan bir form üretir; buradaki ise otomatik doldurur.
  • ElicitResult(action="accept", content={...}) döndürürsünüz; ya da action="decline" veya action="cancel". Bunların dışındaki tek seçenek, isteği reddedip çağrının tamamını başarısız kılan ErrorData(...)'dır.
  • context bir ClientRequestContext'tir: canlı session, sunucunun request_id'si ve eklediği her türlü meta.

Tip

params, iki elicitation kipinin birleşimidir (union). Burada params.mode değeri "form"; bir "url" isteği ise şema yerine params.url taşır. Tek callback ikisini de karşılar; params.mode üzerinden dallanın. Kalıbın tamamı Elicitation sayfasında.

Deneyin

issue_card'ı çağırın ve iki ucu da izleyin.

Callback'iniz sunucunun sorusunu hazır ayrıştırılmış olarak alır:

params.mode              # 'form'
params.message           # 'What name should go on the card?'
params.requested_schema  # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
                         #  'required': ['name'], 'title': 'CardHolder', 'type': 'object'}

Yanıt verir, ctx.elicit(...) aracın içinde kaldığı yerden devam eder ve araç tamamlanır:

result.content  # [TextContent(type='text', text='Card issued to Ada Lovelace.')]

Sizden tek bir tools/call, sunucudan geriye tek bir elicitation/create, onu yanıtlayan da sizin fonksiyonunuz; hepsi tek bir araç çağrısının içinde.

Info

Client(...) çağrısındaki mode="legacy" gerçekten iş yapıyor. Varsayılan olarak Client(...) modern protokol yolunu müzakere eder ve o yolda sunucudan istemciye gelen istekler için bir geri kanal (back-channel) yoktur: ctx.elicit, callback'iniz daha çalışmadan başarısız olur. Buna aktarım karar vermez; müzakere edilen protokol karar verir, bellek içinde de bir URL üzerinden de aynı şekilde. İstemcinizin böyle bir isteği yanıtlaması gerektiğinde mode="legacy"'yi sabitleyin; bu sayfanın arkasındaki her test bunu yapar. Ayrıntıların tamamı Protokol sürümleri sayfasında.

Bir 2026-07-28 oturumunda callback ölü değildir, yalnızca farklı beslenir: bir araç, ElicitRequest taşıyan bir InputRequiredResult döndürdüğünde Client o girdiyi aynı elicitation_callback'e yönlendirir ve çağrıyı sizin adınıza yeniden dener. Bu akış Çok turlu istekler sayfasında.

Callback bir yetenektir

İstemcinizin elicitation isteklerini yanıtlayabildiğini sunucuya hiç söylemediniz. SDK söyledi.

Bir istemci bağlandığında capabilities'ini, yani sunucununkinin aynadaki yansımasını bildirir. O nesneyi siz yazmazsınız. Callback'i kaydetmek bildirimin ta kendisidir.

geçirdiğiniz istemcinin bildirdiği
elicitation_callback= "elicitation": {"form": {}, "url": {}}
sampling_callback= "sampling": {}
list_roots_callback= "roots": {"listChanged": true}
hiçbiri {}

Tek ince ayar örnekleme alt yetenekleridir: örnekleyiciniz tools / tool_choice parametrelerini işliyorsa sampling_callback'in yanında sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability()) geçirin. Sunucular bunları gönderebilmek için önce sampling.tools'un bildirildiğini görmelidir.

logging_callback ve message_handler tabloda yok. Onlar bildirimleri işler ve bildirimler yetenek gerektirmez.

Sunucu bildirimi ctx.session.check_client_capability(...) ile geri okur. Bunu yapan bir araç ekleyin:

server.py
from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Context
from mcp.types import ClientCapabilities, ElicitationCapability, RootsCapability, SamplingCapability

mcp = MCPServer("Library")


class CardHolder(BaseModel):
    name: str


@mcp.tool()
async def issue_card(ctx: Context) -> str:
    """Issue a new library card."""
    answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
    if answer.action == "accept":
        return f"Card issued to {answer.data.name}."
    return "No card issued."


@mcp.tool()
def client_features(ctx: Context) -> list[str]:
    """Which optional features the connected client declared."""
    declared = {
        "elicitation": ClientCapabilities(elicitation=ElicitationCapability()),
        "sampling": ClientCapabilities(sampling=SamplingCapability()),
        "roots": ClientCapabilities(roots=RootsCapability()),
    }
    return [name for name, capability in declared.items() if ctx.session.check_client_capability(capability)]

Yalnızca elicitation_callback ile bağlanın ve aracı çağırın:

result.structured_content  # {'result': ['elicitation']}

Üç callback'i de geçirirseniz ['elicitation', 'sampling', 'roots'] alırsınız. Hiçbirini geçirmezseniz [] alırsınız.

Check

Şimdi yanlış olanı yapın: elicitation_callback olmadan bağlanın ve yine de issue_card'ı çağırın.

Sunucunun elicitation/create isteği yine istemcinize ulaşır ve SDK onu sizin yerinize yanıtlar; ama bir hatayla, çünkü bunu karşılayabileceğinizi hiç söylemediniz. O hata çağrının tamamını batırır. call_tool bir is_error sonucu döndürmez; istisna fırlatır:

MCPError: Elicitation not supported

Bu bir araç hatası değil, bir protokol hatasıdır (-32600, invalid request): modelin okuyup yeniden deneyebileceği bir şey yoktur. client_features'ın değerli olmasının nedeni de bu: uslu bir sunucu sormadan önce kontrol eder.

Kullanım dışı ikili

sampling_callback, sampling/createMessage'ı yanıtlar: sunucunun sizin modelinizden bir şeyi tamamlamasını istemesi. list_roots_callback, roots/list'i yanıtlar: sunucunun hangi dizinlerde çalışabileceğini sorması.

İkisi de çalışır. İkisi de yukarıdaki kurala uyar. Ve ikisi de 2026-07-28 spesifikasyonunun kaldırdığı RPC'lere hizmet eder: modern bir sunucu istek ortasında istemcinizi geri çağırmaz, isteği araç sonucunun bir parçası olarak size geri verir (Çok turlu istekler). Callback'lerin kendisi ölü değildir. Bir InputRequiredResult, CreateMessageRequest veya ListRootsRequest taşıdığında Client'ın otomatik döngüsü onu burada kaydettiğiniz aynı sampling_callback veya list_roots_callback'e yönlendirir. Listenin tamamı Kullanım dışı özellikler sayfasında.

Henüz geçiş yapmamış sunucularla konuşmak için callback'lere hâlâ ihtiyacınız var. İmzalar:

client.py
from pydantic import FileUrl

from mcp.client import ClientRequestContext
from mcp.types import CreateMessageRequestParams, CreateMessageResult, ListRootsResult, Root, TextContent


async def handle_sampling(
    context: ClientRequestContext,
    params: CreateMessageRequestParams,
) -> CreateMessageResult:
    return CreateMessageResult(
        role="assistant",
        content=TextContent(type="text", text="The answer is 42."),
        model="my-llm",
    )


async def handle_list_roots(context: ClientRequestContext) -> ListRootsResult:
    return ListRootsResult(roots=[Root(uri=FileUrl("file:///home/ada/notebooks"), name="notebooks")])
  • Bir örnekleme (sampling) callback'i CreateMessageRequestParams'ın tamamını (messages, model_preferences, max_tokens) alır ve bir CreateMessageResult döndürür. Modeli siz çalıştırırsınız, nasıl isterseniz öyle; SDK yalnızca isteği taşır.
  • Bir kök dizinler (roots) callback'i hiç parametre almaz ve bir ListRootsResult döndürür.
  • Her ikisi de reddetmek için bunun yerine ErrorData(...) döndürebilir.

Bunları Client(...)'a tıpkı elicitation_callback gibi geçirin.

Bildirim callback'leri

İki tane daha. Hiçbiri bir şey bildirmez.

logging_callback, sunucunun gönderdiği notifications/messageLoggingMessageNotificationParams (level, logger, data) olarak alır. Protokol log'lamasının kendisi 2026-07-28 spesifikasyonuyla kullanım dışı bırakıldı (yerine ne yapılacağı Log kaydı sayfasında); bu yüzden bu callback, hâlâ bunu yayan sunucular için var. 2026 neslinden bir bağlantıda callback tek başına size hiçbir şey kazandırmaz, çünkü 2026 sunucuları log mesajlarını yalnızca bunu talep eden isteklere gönderir: bu talebi her isteğe damgalamak ve o düzey ile üstünü almak için Client(...)'a log_level="info" (veya başka bir düzey) geçirin. 2026 öncesi sunucular bunu yok sayar ve logging/setLevel davranışlarını sürdürür.

message_handler her şeyi yakalayandır: oturumun yüzeye çıkardığı her sunucu bildirimi ona ulaşır (kendi özel callback'inin yanı sıra), akış tabanlı bir aktarımda aktarım düzeyindeki her Exception da öyle. İkisi asla ulaşmaz: notifications/cancelled yüzeye çıkarılmak yerine SDK tarafından uygulanır ve canlı bir listen() akışının abonelik onayı o akış tarafından tüketilir. Parametreye IncomingMessage (ServerNotification | Exception, mcp.client'tan dışa aktarılır) tür ipucunu verin. Bilmeye değer tek kalıp if isinstance(message, Exception): raise message'dır; böylece kopan bir bağlantı sessizce kaybolmak yerine gürültüyle başarısız olur.

Özet

  • Sunucu istemciye istek gönderebilir. Bunları Client(...)'a geçirdiğiniz callback'lerle yanıtlarsınız.
  • Güncel olan elicitation callback'idir: async (context, params) -> ElicitResult, hem form hem URL kipi için tek fonksiyon.
  • Callback'i kaydetmek yeteneği bildirmektir. O olmadan SDK sunucunun isteğini sizin adınıza reddeder ve çağrının tamamı MCPError ile başarısız olur.
  • Sunucu, sormadan önce ctx.session.check_client_capability(...) ile öğrenir.
  • sampling_callback ve list_roots_callback aynı şekilde çalışır ama kullanım dışı özelliklere hizmet eder; modern sunucular bunun yerine çok turlu istekler (multi-round-trip) kullanır.
  • logging_callback ve message_handler bildirimleri alır. Hiçbir şey bildirmezler.

Client(...)'ın ilk argümanı bir aktarım nesnesidir. İstemci aktarımları her türünü ele alır.