Перейти до змісту

Колбеки клієнта

Машинний переклад

Цю сторінку перекладено автоматично з англомовної документації, і основною версією є англомовна сторінка. Якщо щось читається неправильно, на сторінці Переклади пояснено, як про це повідомити.

Майже кожен запит у MCP іде в один бік: від клієнта до сервера.

Сервер теж може дещо попросити в клієнта: поставити запитання користувачеві, скористатися моделлю користувача для семплювання (sampling), отримати список папок його робочого простору. На ці запити відповідають колбеки, які передаються в Client(...).

Сервер, який запитує

Ось сервер, інструмент якого не може завершитися самотужки:

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(...) надсилає запит elicitation/create клієнтові й чекає.
  • Інструмент не повертає результат, доки хтось (людина у формі або ваш код) не надасть name.

Це серверна половина, і вона належить сторінці Еліцитація. Ця сторінка — про інший кінець з'єднання.

Колбек еліцитації

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)
  • Колбек еліцитації (elicitation) — це async (context, params) -> ElicitResult.
  • params.message — це запитання. params.requested_schema — JSON Schema відповіді, яку хоче отримати сервер. Справжній клієнт будує з неї форму; цей заповнює її автоматично.
  • Повертається ElicitResult(action="accept", content={...}), або action="decline", або action="cancel". Єдиний інший варіант — ErrorData(...): він відхиляє запит, і весь виклик завершується помилкою.
  • context — це ClientRequestContext: активна session, request_id сервера та будь-які meta, які він додав.

Tip

params — об'єднання двох режимів еліцитації. Тут params.mode дорівнює "form"; запит "url" замість схеми несе params.url. Обидва обробляє один колбек; розгалужуйтеся за params.mode. Повний шаблон показано на сторінці Еліцитація.

Спробуйте самі

Викличте issue_card і простежте за обома кінцями.

Колбек отримує запитання сервера, уже розібране:

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'}

Він відповідає, ctx.elicit(...) усередині інструмента відновлює роботу, й інструмент завершується:

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

Один tools/call від вас, один elicitation/create у відповідь від сервера, на який відповіла ваша функція, — і все це всередині одного виклику інструмента.

Info

mode="legacy" у виклику Client(...) стоїть не просто так. За замовчуванням Client(...) узгоджує сучасний шлях протоколу, а на ньому немає зворотного каналу (back-channel) для запитів від сервера до клієнта: ctx.elicit завершується помилкою ще до того, як запуститься колбек. Вирішує це не транспорт, а узгоджений протокол — однаково і в пам'яті, і за URL. Фіксуйте mode="legacy" щоразу, коли клієнт має відповідати на такий запит; так робить кожен тест за цією сторінкою. Докладніше — на сторінці Версії протоколу.

У сесії 2026-07-28 колбек не зникає, він просто отримує дані інакше: коли інструмент повертає InputRequiredResult з ElicitRequest усередині, Client передає цей запис тому самому elicitation_callback і повторює виклик за вас. Цей сценарій описано на сторінці Багатораундові запити (multi-round-trip).

Колбек — це можливість

Ви ніде не повідомляли серверу, що ваш клієнт уміє відповідати на запити еліцитації. Це зробив SDK.

Під'єднуючись, клієнт оголошує свої capabilities — дзеркальне відображення серверних. Цей об'єкт ви не пишете. Реєстрація колбека і є оголошенням.

що передається що оголошує клієнт
elicitation_callback= "elicitation": {"form": {}, "url": {}}
sampling_callback= "sampling": {}
list_roots_callback= "roots": {"listChanged": true}
жодного з них {}

Єдине уточнення — підможливості семплювання: передавайте sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability()) разом із sampling_callback, якщо ваш семплер обробляє параметри tools / tool_choice. Сервери мають побачити оголошену sampling.tools, перш ніж надсилати їх.

logging_callback і message_handler у таблиці немає. Вони обробляють сповіщення, а сповіщенням можливість не потрібна.

Сервер зчитує оголошення методом ctx.session.check_client_capability(...). Додайте інструмент, який це робить:

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)]

Під'єднайтеся лише з elicitation_callback і викличте його:

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

Передайте всі три колбеки — отримаєте ['elicitation', 'sampling', 'roots']. Не передайте жодного — отримаєте [].

Check

Тепер зробіть неправильно: під'єднайтеся без elicitation_callback і все одно викличте issue_card.

Запит сервера elicitation/create все одно доходить до клієнта, і SDK відповідає на нього за вас — помилкою, бо ви ніде не сказали, що можете його обробити. Ця помилка топить весь виклик. call_tool не повертає результат із is_error; він викидає виняток:

MCPError: Elicitation not supported

Це помилка протоколу (-32600, invalid request), а не помилка інструмента: моделі тут нічого прочитати й повторити. Саме тому client_features варто мати: чемний сервер перевіряє, перш ніж питати.

Застаріла пара

sampling_callback відповідає на sampling/createMessage: сервер просить вашу модель щось доповнити. list_roots_callback відповідає на roots/list: сервер питає, у яких каталогах йому можна працювати.

Обидва працюють. Обидва дотримуються правила вище. І обидва обслуговують RPC, які специфікація 2026-07-28 вилучає: сучасний сервер не звертається до клієнта посеред запиту, а повертає запит вам як частину результату інструмента (Багатораундові запити). Самі колбеки нікуди не зникають. Коли InputRequiredResult несе CreateMessageRequest або ListRootsRequest, автоматичний цикл Client передає його тому самому sampling_callback чи list_roots_callback, який ви зареєстрували тут. Повний список — на сторінці Застарілі можливості.

Колбеки досі потрібні, щоб спілкуватися із серверами, які ще не перейшли. Сигнатури:

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")])
  • Колбек семплювання отримує повний CreateMessageRequestParams (messages, model_preferences, max_tokens) і повертає CreateMessageResult. Модель запускаєте ви, як завгодно; SDK лише переносить запит.
  • Колбек кореневих каталогів (roots) не приймає жодних параметрів і повертає ListRootsResult.
  • Кожен із них натомість може повернути ErrorData(...), щоб відмовити.

Передавайте їх у Client(...) так само, як elicitation_callback.

Колбеки сповіщень

Ще два. Жоден нічого не оголошує.

logging_callback отримує notifications/message, які надсилає сервер, у вигляді LoggingMessageNotificationParams (level, logger, data). Протокольне логування саме оголошене застарілим у специфікації 2026-07-28 (що робити натомість — на сторінці Логування), тож цей колбек існує для серверів, які досі його надсилають. На з'єднанні покоління 2026 самого колбека недостатньо, бо сервери 2026 надсилають лог-повідомлення лише у відповідь на запити, які на це погодилися: передайте log_level="info" (або інший рівень) у Client(...), щоб проставляти цю згоду в кожному запиті й отримувати повідомлення цього рівня та вище. Сервери до 2026 ігнорують її й зберігають свою поведінку logging/setLevel.

message_handler — універсальний приймач: до нього доходить кожне сповіщення сервера, яке сесія передає назовні (на додачу до свого спеціального колбека), а на транспорті на основі потоку — ще й кожен Exception транспортного рівня. Два ніколи не доходять: notifications/cancelled SDK застосовує сам, а не передає назовні, а підтвердження підписки для активного потоку listen() споживає сам цей потік. Анотуйте параметр типом IncomingMessage (ServerNotification | Exception, експортується з mcp.client). Єдиний шаблон, який варто знати, — if isinstance(message, Exception): raise message, щоб розірване з'єднання падало гучно, а не зникало безслідно.

Підсумки

  • Сервер може надсилати запити клієнтові. Відповідають на них колбеки, передані в Client(...).
  • Колбек еліцитації — актуальний: async (context, params) -> ElicitResult, одна функція і для режиму форми, і для режиму URL.
  • Реєстрація колбека — це оголошення можливості. Без нього SDK відхиляє запит сервера від вашого імені, і весь виклик завершується з MCPError.
  • Сервер дізнається про це ще до запиту за допомогою ctx.session.check_client_capability(...).
  • sampling_callback і list_roots_callback працюють так само, але обслуговують застарілі можливості; сучасні сервери натомість використовують багатораундові запити.
  • logging_callback і message_handler отримують сповіщення. Вони нічого не оголошують.

Перший аргумент Client(...) — об'єкт транспорту. Усі його різновиди описано на сторінці Транспорти клієнта.