Колбэки клиента
Машинный перевод
Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.
Почти все запросы в MCP идут в одну сторону: от клиента к серверу.
Но и сервер может о чём-то попросить клиент: задать вопрос пользователю, попросить модель пользователя сгенерировать ответ, получить список папок его рабочего пространства. На такие запросы отвечают колбэки, которые передаются в Client(...).
Сервер, который спрашивает
Вот сервер, инструмент которого не может завершиться самостоятельно:
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.
Это серверная половина, и ей посвящена страница Элицитация. Эта страница — о другом конце соединения.
Колбэк элицитации
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): передайте sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability()) вместе с sampling_callback, если ваш обработчик сэмплирования поддерживает параметры tools / tool_choice. Сервер может отправлять их, только когда видит объявленную sampling.tools.
logging_callback и message_handler в таблице нет. Они обрабатывают уведомления, а уведомлениям возможность не нужна.
Сервер читает это объявление с помощью ctx.session.check_client_capability(...). Добавьте инструмент, который это делает:
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, который вы зарегистрировали здесь. Полный список — на странице Устаревшие возможности.
Колбэки по-прежнему нужны, чтобы общаться с серверами, которые ещё не перешли. Сигнатуры:
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(...) — объект транспорта. Все их виды описаны на странице Транспорты клиента.