コンテンツにスキップ

エリシテーション(elicitation)

このページは機械翻訳です

このドキュメントの翻訳版は英語版のページから自動的に生成されており、正式な内容はこのページの英語版です。

翻訳におかしな点を見つけましたか?翻訳の仕組みと問題の報告方法を参照してください。

処理の途中まで進んだツールが、答えを 1 つ欠いているからといって失敗する必要はありません。

エリシテーションを使えば、ツールの側から尋ねられます。ツール呼び出しの途中でユーザーに質問が表示され、その答えが同じ関数呼び出しの中へ戻ってきます。

モードは 2 つあります。

  • フォームモード:値が必要な場合(確認、日付、数量など)。フィールドを記述すると、クライアントがフォームを描画します。
  • URL モード:ユーザーに別の場所へ移動してもらう場合(OAuth の同意画面、決済ページなど)。そこでの操作はプロトコルを一切通りません。

そして、尋ね方も 2 通りあります。まず選ぶべきなのはリゾルバーです。質問をパラメーターに結び付けておけば、SDK が尋ねてくれます。どんな接続でも、クライアントが話すプロトコルの世代が何であっても動作します。直接的な方法である await ctx.elicit(...) は、サーバーからクライアントへのリクエストであり、このチャネルはレガシー接続(仕様バージョン 2025-11-25 以前)のクライアントにしか存在しません。このページでは両方を扱いますが、まずはリゾルバーから始めましょう。

リゾルバーで尋ねる

ツール全体の前提となる質問(「本当に実行しますか?」「一致した 3 つのアカウントのうちどれですか?」)は、ツール本体からリゾルバーへ切り出せます。あとはフレームワークが代わりに尋ねてくれます。

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_folderElicitationResult[Confirm] をアノテーションしているため、フレームワークは結果全体を注入し、ツールは match ですべてのケースを処理します。承認して確定、承認したが保持(ok=False)、拒否、キャンセルです。
  • confirm パラメーターはツールの入力スキーマには現れません。クライアントが path を渡し、リゾルバーが confirm を供給します。

ツール側で分岐する必要がない場合は、代わりにラップしていないモデル(Annotated[Confirm, Resolve(confirm_delete)])をアノテーションしてください。承認時にはモデルを受け取り、拒否やキャンセルの場合は呼び出しがエラーで中断されます。

リゾルバーはすべての接続で動作します。レガシー接続のクライアントには、SDK が質問を直接送ります。2026-07-28 の接続では、SDK は質問を呼び出しの戻り値として返し、クライアントの次の試行が答えを運んできます。リゾルバー側がその違いを知ることはありません。内部で起きていることは マルチラウンドトリップ(multi-round-trip)リクエスト を参照してください。

尋ねることは、リゾルバーができることの 1 つにすぎません。一般的な仕組み(尋ねずに計算する依存関係、依存関係の依存関係、モデルが供給できるものとできないもの)については、依存関係 のページを参照してください。

ツールの中から尋ねる

ツールは、自身の本体の途中で止まって尋ねることもできます。

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."
  • ctx.elicit を使えるようにしてくれるのが Context パラメーターです。どのツールでも受け取れます。このオブジェクトには専用のページがあります: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

エリシテーションのスキーマは、ツールの入力スキーマほど表現力がありません。使えるのは フラットでプリミティブなフィールドだけです:strintfloatbool、または文字列の Literalenum になります)。モデルの中にモデルを入れると、クライアントへ何かが送られる 前に ctx.elicit が例外を送出します。

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

相手は作業中の人間です。答えにネストが必要なら、それはツールの引数にすべきだったものです。

3 つの答え

result.action を見れば、ユーザーが何をしたか分かります。可能性はちょうど 3 つです。

  • "accept":フォームを送信しました。result.data は検証済みの AlternativeDate インスタンスです。
  • "decline":拒否されました。
  • "cancel":選択せずに質問を閉じました。

result.data"accept" の場合にしか存在しません。例で先に result.action を確認しているのはそのためです。型チェッカーもこの順序を強制します。result.action == "accept" の後では result.dataAlternativeDate ですが、その前には .data はそもそも存在しません。

拒否はエラーではありません。拒否が何を意味するか(ここでは予約しないこと)はツールが決め、モデルには通常どおり応答します。

Tip

答えは、コードが受け取る前にモデルで検証されます。bool に対して "maybe" を送る クライアントがいても予約が壊れることはありません。呼び出しはスキーマ不一致のエラーで 失敗し、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 を受け取ります。elicitation_id は、サーバー内でこのエリシテーションを識別できる任意の文字列です。
  • 結果にはアクションしか含まれません。"accept" はユーザーが URL を開くことに同意したという意味であり、その先の手続きを完了したという意味ではありません
  • 決済は、ユーザーのブラウザーと決済プロバイダーの間で、プロトコルの外側で行われます。コンテンツが MCP を通って戻ってくることはありません。

2 つ目のツールを見てください。プロトコル外のフローが完了したことをサーバーが知ったとき(Webhook やポーリングなど。ここでは 2 つ目のツールとしてモデル化しています)、ctx.session.send_elicit_complete(...) が同じ elicitation_id を付けて notifications/elicitation/complete を送ります。これによってクライアントは「支払いを待っています...」の表示をやめてよいと分かります。これがないと、クライアントは推測するしかありません。

クライアント側

尋ねるのはサーバーです。クライアントは Client(...)elicitation_callback を渡して答えます。

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)
  • 1 つのコールバックで両方のモードを処理します。paramsElicitRequestFormParamsElicitRequestURLParams の union で、isinstance が分岐点です。
  • URL の場合は params.url をユーザーに提示し、選ばれたアクションを返します。content は決して返しません。
  • フォームの場合、実際のアプリケーションは params.requested_schema を描画し、ユーザーの入力を content として返します。この例は常に決まった答えで承認しますが、テストではまさにこうしたコールバックが欲しくなります。
  • コールバックを渡すことは、ケイパビリティの宣言でもあります。これによってサーバーは、このクライアントに尋ねられると分かります。クライアントがサーバーのために答えられるその他のものについては、クライアントコールバック を参照してください。

Info

エリシテーションはサーバーからクライアントへのリクエストであり、それは従来のハンドシェイクの セッションにしか存在しません。このクライアントが mode="legacy" を渡しているのはそのためです。 2026-07-28 の接続では、ツールは呼び出しの戻り値として質問を返すことで尋ねます。 そのフローが マルチラウンドトリップリクエスト です。

試してみる

ctx.elicit を使うフォームモードの server.pybook_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.

次に URL モードの server.py に差し替え、同じ main()pay_deposit に向けてみてください。同じコールバックがもう一方の分岐に入り、決済リンクを出力し、ツールは「Complete the payment in your browser.」を返します。呼び出しの途中で、双方向に 1 往復です。

Check

ここで Client から elicitation_callback= を外し、もう一度クリスマス当日で book_table を 呼んでみてください。呼び出し全体がプロトコルエラーで失敗します。

Elicitation not supported

コールバックを登録していないクライアントは elicitation ケイパビリティを宣言していないため、 尋ねる相手がいません。ツールが受け取ったのは "decline" ではなく、例外です。これを前提に 設計してください。どのエリシテーションにも、「尋ねられなかったらどうするか」への妥当な答えが必要です。

まとめ

  • Annotated[T, Resolve(fn)] を付けたパラメーターはリゾルバーによって埋められ、リゾルバーは尋ねる必要があるときに Elicit(...) を返します。すべての接続で動作します。
  • スキーマはフラットな Pydantic モデルです。プリミティブなフィールドのみで、戻ってくる際に検証されます。
  • result.action"accept""decline""cancel" のいずれかで、result.data は accept のときだけ存在します。
  • await ctx.elicit(message, schema=Model) はツール本体の中から尋ね、await ctx.elicit_url(message, url, elicitation_id) はモデルを通してはいけないすべてのもののためにあります(ctx.session.send_elicit_complete(elicitation_id) が、プロトコル外の処理が終わったことを伝えます)。どちらもサーバーからクライアントへのリクエストなので、レガシー接続のクライアントが必要です。
  • クライアントは 1 つの elicitation_callback で答え、params の型で分岐します。これを登録することがケイパビリティの宣言になります。
  • 2026-07-28 の接続では、サーバーは質問を送りつけるのではなく返します。同じコールバックに値を渡すのは マルチラウンドトリップリクエスト です。

その戻り値の裏側にあるもの(リトライループ、requestState の保護、自分で駆動する方法)はすべて マルチラウンドトリップリクエスト にあります。