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

Еліцитація

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

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

Інструмент, який уже наполовину виконав свою роботу й не має однієї відповіді, не мусить завершуватися помилкою.

Еліцитація (elicitation) дає йому змогу запитати. Посеред виклику інструмента користувач отримує запитання, а його відповідь повертається в той самий виклик функції.

Є два режими:

  • Режим форми: потрібне значення (підтвердження, дата, кількість). Ви описуєте поля, клієнт відображає форму.
  • Режим URL: потрібно, щоб користувач перейшов кудись іще (екран згоди OAuth, сторінка оплати). Ніщо з того, що він там робить, не проходить через протокол.

І є два способи запитати. Той, до якого варто звертатися насамперед, — резолвер: запитання навішується на параметр, а питає SDK — на будь-якому з'єднанні, хоч би якого покоління протоколу дотримувався клієнт. Прямий спосіб, await ctx.elicit(...), — це запит від сервера до клієнта, канал, що існує лише для клієнта на з'єднанні старого покоління (версія специфікації 2025-11-25 або раніша). Обидва способи є на цій сторінці; почніть із резолвера.

Запитання через резолвер

Запитання, від якого залежить увесь інструмент, — ви впевнені? який із трьох знайдених облікових записів? — можна винести з тіла інструмента в резолвер, і фреймворк поставить його за вас.

Параметр з анотацією Annotated[T, Resolve(fn)] заповнюється запуском fn перед тілом інструмента. Резолвер повертає значення безпосередньо, коли вже його знає, або повертає Elicit(...), щоб запитав фреймворк:

server.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import (
    AcceptedElicitation,
    CancelledElicitation,
    DeclinedElicitation,
    Elicit,
    ElicitationResult,
    Resolve,
)

mcp = MCPServer("Files")

_FOLDERS: dict[str, list[str]] = {"/tmp/empty": [], "/tmp/project": ["main.py", "README.md"]}


class Confirm(BaseModel):
    ok: bool


async def confirm_delete(path: str) -> Confirm | Elicit[Confirm]:
    """Resolver: ask for confirmation only when the folder is not empty."""
    file_count = len(_FOLDERS.get(path, []))
    if file_count == 0:
        return Confirm(ok=True)  # nothing to confirm, no round-trip to the client
    return Elicit(f"{path} has {file_count} file(s). Delete anyway?", Confirm)


@mcp.tool()
async def delete_folder(
    path: str,
    confirm: Annotated[ElicitationResult[Confirm], Resolve(confirm_delete)],
) -> str:
    """Delete a folder, asking for confirmation when it is not empty."""
    match confirm:
        case AcceptedElicitation(data=Confirm(ok=True)):
            _FOLDERS.pop(path, None)
            return f"deleted {path}"
        case AcceptedElicitation():
            return "kept the folder"
        case DeclinedElicitation():
            return "declined: folder not deleted"
        case CancelledElicitation():
            return "cancelled: folder not deleted"
  • confirm_delete читає власний аргумент інструмента path за іменем, переглядає вміст папки й питає лише тоді, коли мусить — порожня папка розв'язується в Confirm(ok=True) без жодного раунду обміну з клієнтом.
  • delete_folder анотує ElicitationResult[Confirm], тож фреймворк впроваджує весь результат цілком, і інструмент через match обробляє кожен випадок: прийняти й підтвердити, прийняти, але залишити (ok=False), відхилити, скасувати.
  • Параметр confirm ніколи не з'являється у вхідній схемі інструмента — клієнт надає path, резолвер надає confirm.

Коли інструменту не потрібне розгалуження, анотуйте натомість розгорнуту модель (Annotated[Confirm, Resolve(confirm_delete)]): у разі прийняття він отримує модель, а в разі відхилення чи скасування виклик переривається з помилкою.

Резолвер працює на кожному з'єднанні. Клієнтові на з'єднанні старого покоління SDK надсилає запитання напряму; на з'єднанні 2026-07-28 SDK повертає запитання з виклику, а наступна спроба клієнта несе відповідь. Резолвер ніколи не помічає різниці; те, що відбувається всередині, описано на сторінці Багатораундові запити (multi-round-trip).

Запитувати — лише одне з того, що вміє резолвер. Загальний механізм — залежності, що обчислюються без запитань, залежності залежностей, що модель може й чого не може надати — описано на сторінці Залежності.

Запитання зсередини інструмента

Інструмент також може зупинитися посеред власного тіла й запитати.

Warning

ctx.elicit() і ctx.elicit_url() — це запити від сервера до клієнта, канал, що існує лише для клієнта на з'єднанні старого покоління (версія специфікації 2025-11-25 або раніша). На з'єднанні 2026-07-28 запитів, ініційованих сервером, немає, тож ці виклики завершуються помилкою. Резолвер працює в обох випадках. Докладніше — на сторінці Версії протоколу.

await ctx.elicit() приймає повідомлення й модель Pydantic:

server.py
from pydantic import BaseModel, Field

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

mcp = MCPServer("Bistro")


class AlternativeDate(BaseModel):
    accept_alternative: bool = Field(description="Try another date?")
    date: str = Field(default="2025-12-26", description="Alternative date (YYYY-MM-DD)")


@mcp.tool()
async def book_table(date: str, party_size: int, ctx: Context) -> str:
    """Book a table at the bistro."""
    if date != "2025-12-25":
        return f"Booked a table for {party_size} on {date}."

    result = await ctx.elicit(
        message=f"No tables for {party_size} on {date}. Would you like to try another date?",
        schema=AlternativeDate,
    )
    if result.action == "accept" and result.data.accept_alternative:
        return await book_table(result.data.date, party_size, ctx)
    return "No booking made."
  • Параметр Context — це те, що дає ctx.elicit; будь-який інструмент може його приймати. Цей об'єкт має власну сторінку: Об'єкт Context.
  • AlternativeDate — це схема відповіді, яку ви хочете отримати.
  • Інструмент оголошено як async def. Інакше не можна: він зупиняється посередині й чекає на людину.
  • Для будь-якої іншої дати інструмент повертає результат одразу. Він питає лише тоді, коли мусить.
  • Дата, яку приймає користувач, знову проходить через сам book_table. Відповідь — це такі самі вхідні дані, як і будь-які інші: про альтернативу, яка теж повністю заброньована, запитають знову, а не підтвердять наосліп.

Що отримує клієнт

Клієнт отримує ваше повідомлення, а поруч із ним — JSON Schema, згенеровану з моделі:

{
  "properties": {
    "accept_alternative": {
      "description": "Try another date?",
      "title": "Accept Alternative",
      "type": "boolean"
    },
    "date": {
      "default": "2025-12-26",
      "description": "Alternative date (YYYY-MM-DD)",
      "title": "Date",
      "type": "string"
    }
  },
  "required": ["accept_alternative"],
  "title": "AlternativeDate",
  "type": "object"
}

Ця схема і є формою. Field(description=...) — це підпис; значення за замовчуванням попередньо заповнює поле введення й робить поле необов'язковим. Це той самий механізм перетворення Pydantic на JSON Schema, який сторінка Інструменти описує для аргументів інструмента.

Warning

Схема еліцитації не така виразна, як вхідна схема інструмента. Лише пласкі примітивні поля: str, int, float, bool або Literal з рядків (він стає enum). Покладіть модель усередину моделі — і ctx.elicit викине виняток ще до того, як щось буде надіслано клієнтові:

TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition

Ви перериваєте людину посеред справи. Якщо відповідь потребує вкладеності, вона мала б бути аргументом інструмента.

Три відповіді

result.action повідомляє, що зробив користувач, і можливостей рівно три:

  • "accept": він надіслав форму. result.data — це екземпляр AlternativeDate, уже провалідований.
  • "decline": він відмовив.
  • "cancel": він закрив запитання, нічого не вибравши.

result.data існує лише для "accept", тому приклад спершу перевіряє result.action. Засіб перевірки типів стежить за порядком: після result.action == "accept" result.data — це AlternativeDate; до того .data немає взагалі.

Відмова — не помилка. Інструмент сам вирішує, що означає відхилення (тут — бронювання немає), і відповідає моделі як звичайно.

Tip

Відповідь валідується за вашою моделлю, перш ніж її побачить ваш код. Клієнт, що надсилає "maybe" для bool, не зіпсує бронювання: виклик завершується помилкою невідповідності схемі, а ваш if так і не виконується.

Перенаправлення користувача на URL

Деякі речі не повинні проходити ні через модель, ні через клієнта: облікові дані, номери карток, згода OAuth. Для них ви не просите даних; ви просите користувача кудись перейти:

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

mcp = MCPServer("Bistro")


@mcp.tool()
async def pay_deposit(booking_id: str, ctx: Context) -> str:
    """Take the deposit that confirms a booking."""
    result = await ctx.elicit_url(
        message="A 20 EUR deposit confirms your booking.",
        url=f"https://pay.example.com/deposit/{booking_id}",
        elicitation_id=f"deposit-{booking_id}",
    )
    if result.action == "accept":
        return "Complete the payment in your browser."
    return "No deposit taken. The booking expires in one hour."


@mcp.tool()
async def confirm_deposit(booking_id: str, ctx: Context) -> str:
    """Record a payment reported by the payment provider."""
    await ctx.session.send_elicit_complete(f"deposit-{booking_id}")
    return f"Deposit received for booking {booking_id}."
  • ctx.elicit_url() приймає повідомлення, URL, який треба відвідати, і elicitation_id, який ви обираєте самі: будь-який рядок, що ідентифікує цю еліцитацію в межах вашого сервера.
  • Результат містить дію й більше нічого. "accept" означає, що користувач погодився відкрити URL, а не те, що він завершив усе на тому боці.
  • Оплата відбувається поза протоколом, між браузером користувача й вашим платіжним провайдером. Жоден вміст ніколи не повертається через MCP.

Погляньте на другий інструмент. Коли сервер дізнається, що зовнішній процес завершено (вебхук, опитування; тут це змодельовано як другий інструмент), ctx.session.send_elicit_complete(...) надсилає notifications/elicitation/complete з тим самим elicitation_id. Саме так клієнт дізнається, що можна припинити показувати «очікуємо оплату...». Без цього клієнтові залишається лише здогадуватися.

На боці клієнта

Сервери питають. Клієнти відповідають, передаючи elicitation_callback у Client(...):

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


async def handle_elicitation(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
    if isinstance(params, ElicitRequestURLParams):
        print(f"Open this link to continue: {params.url}")
        return ElicitResult(action="accept")
    print(params.message)
    return ElicitResult(action="accept", content={"accept_alternative": True, "date": "2025-12-27"})


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("book_table", {"date": "2025-12-25", "party_size": 2})
        print(result.content)
  • Один колбек обробляє обидва режими. params — це об'єднання ElicitRequestFormParams і ElicitRequestURLParams; розгалуження робиться через isinstance.
  • Для URL ви показуєте користувачеві params.url і повертаєте дію, яку він обрав. Ніколи жодного content.
  • Для форми справжній застосунок відображає params.requested_schema і повертає введене користувачем як content. Цей завжди каже «так» із заготовленою відповіддю — саме такий колбек і потрібен у тесті.
  • Передавання колбека — це водночас і оголошення можливості: саме так сервер дізнається, що цього клієнта можна питати. Інше, на що клієнт може відповідати серверові, описано на сторінці Колбеки клієнта.

Info

Еліцитація — це запит від сервера до клієнта, а такі існують лише в сесії з класичним рукостисканням, тому цей клієнт передає mode="legacy". На з'єднанні 2026-07-28 інструмент натомість питає, повертаючи запитання з виклику; цей потік описано на сторінці Багатораундові запити.

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

Запустіть server.py у режимі форми з ctx.elicit (той, що з book_table) через Streamable HTTP (потрібний однорядковий приклад є на сторінці Запуск сервера), потім виконайте main() клієнта й викличте book_table на день Різдва.

Колбек виводить запитання, яке йому надіслали:

No tables for 2 on 2025-12-25. Would you like to try another date?

Він відповідає {"accept_alternative": True, "date": "2025-12-27"}, а інструмент, який увесь цей час чекав усередині await ctx.elicit(...), завершує бронювання:

Booked a table for 2 on 2025-12-27.

Тепер підставте server.py у режимі URL і спрямуйте ту саму main() на pay_deposit: той самий колбек піде іншою гілкою, виведе посилання на оплату, а інструмент повернеться з «Complete the payment in your browser.». Один раунд обміну, посеред виклику, в обох напрямках.

Check

Тепер приберіть elicitation_callback= із Client і знову викличте book_table на день Різдва. Увесь виклик завершується помилкою протоколу:

Elicitation not supported

Клієнт, що не зареєстрував колбека, ніколи не оголошував можливості elicitation, тож питати нікого. Інструмент не отримав "decline"; він отримав виняток. Проєктуйте з огляду на це: кожна еліцитація потребує розумної відповіді на запитання «а що, як запитати не можна?».

Підсумки

  • Параметр з анотацією Annotated[T, Resolve(fn)] заповнює резолвер, який повертає Elicit(...), коли мусить запитати. Це працює на кожному з'єднанні.
  • Схема — пласка модель Pydantic: лише примітивні поля, валідовані на зворотному шляху.
  • result.action — це "accept", "decline" або "cancel"; result.data існує лише в разі прийняття.
  • await ctx.elicit(message, schema=Model) питає зсередини тіла інструмента, а await ctx.elicit_url(message, url, elicitation_id) — для всього, що не повинно проходити через модель (ctx.session.send_elicit_complete(elicitation_id) повідомляє, що зовнішню частину завершено). Обидва — запити від сервера до клієнта: їм потрібен клієнт на з'єднанні старого покоління.
  • Клієнт відповідає одним elicitation_callback, розгалужуючись за типом params; його реєстрація й оголошує можливість.
  • На з'єднанні 2026-07-28 сервер повертає запитання замість того, щоб надсилати його; той самий колбек отримує дані через Багатораундові запити.

Усе, що стоїть за цим поверненням (цикл повторних спроб, захист requestState, самостійне керування), описано на сторінці Багатораундові запити.