コンテンツにスキップ

依存関係

機械翻訳

このページは英語版ドキュメントから自動翻訳されたものであり、正式な版は英語版のページです。不自然な箇所があれば、翻訳についてで報告の方法を説明しています。

ツールの引数はモデルから渡されます。しかし、モデルから渡されるべきではない値もあります。記録から調べた価格、人間にしか出せない確認、モデルがでっち上げると間違えかねないあらゆる値です。

依存関係とは、自分の関数で埋めるパラメーターです。パラメーターに注釈を付けて関数を指定すると、ツールが実行される前に SDK がその関数を呼び出します。

宣言する

パラメーターの型を Annotated[...] で包み、Resolve(fn) を追加します。

server.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


@mcp.tool()
async def reserve_book(title: str, stock: Annotated[Stock, Resolve(check_stock)]) -> str:
    """Reserve a copy of a book."""
    if stock.copies == 0:
        return f"{title!r} is out of stock."
    return f"Reserved {title!r} ({stock.copies - 1} copies left)."
  • check_stockリゾルバーです。SDK が reserve_book の前に実行する普通の関数で、その戻り値が stock 引数になります。
  • その title パラメーターはツール自身の title 引数で、名前で照合されます。リゾルバーが受け取るのは、ツール本体が受け取るのとまったく同じ、検証済みの値です。
  • ツール本体は、すでに存在する Stock から始まります。ツールの中に在庫を調べるコードはなく、「見つからなかったら」という前置きもありません。

Info

FastAPI を使ったことがあれば、これは Depends です。同じ仕組みで、同じ理由です。関数が必要なものを宣言し、フレームワークがそれを供給し、配線は型注釈の中にあります。

モデルからは見えない

tools/listreserve_book について報告する入力スキーマは次のとおりです。

{
  "type": "object",
  "properties": {
    "title": {"title": "Title", "type": "string"}
  },
  "required": ["title"],
  "title": "reserve_bookArguments"
}

プロパティは 1 つです。ContextContext と同じく、解決されるパラメーターは自分と SDK の間の取り決めです。stock はスキーマに含まれず、モデルには一切知らされません。それでも stock の値を送ってくるクライアントがあっても、その値は無視されます。ツールが受け取れるのはリゾルバーの値だけです。

肝心なのは最後の部分です。モデルが渡せないパラメーターは、モデルが間違えようのないパラメーターです。

試してみる

MCP Inspector でサーバーを実行します。

uv run mcp dev server.py

reserve_book のフォームには title フィールドが 1 つあるだけです。stock はどこにもありません。Dune で呼び出してみてください。

Reserved 'Dune' (6 copies left).

ツール本体は何も調べていません。先に check_stock が実行され、それが返した Stock が引数として届きました。Neuromancer を試すと、同じリゾルバーがツールにゼロを渡します。

Tip

ツール本体で check_stock(title) を呼ぶだけでも済みます。依存関係として宣言するのは、その値がヘルパー呼び出し以上の扱いに値するときです。在庫を必要とするツールはどれも同じパラメーターを宣言し、いくつのツールが宣言していても、SDK はリゾルバーを 1 回の呼び出しにつき最大 1 回しか実行しません。残りは次のセクションで扱います。互いに依存するリゾルバーと、ユーザーに質問するリゾルバーです。

依存関係の依存関係

リゾルバーは、同じ注釈を使って自分自身の依存関係を宣言できます。

server.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


async def estimate_delivery(stock: Annotated[Stock, Resolve(check_stock)]) -> str:
    return "tomorrow" if stock.copies > 0 else "in 2-3 weeks"


@mcp.tool()
async def order_book(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
    delivery: Annotated[str, Resolve(estimate_delivery)],
) -> str:
    """Order a book from the shop."""
    if stock.copies == 0:
        return f"{title!r} is on backorder; it would arrive {delivery}."
    return f"Ordered {title!r}; it arrives {delivery}."
  • estimate_deliverycheck_stock に依存しています。SDK はグラフを順番に実行します。まず在庫、次に見積もり、最後にツールです。
  • stockdelivery も最終的には check_stock を必要としますが、実行されるのは1 回の呼び出しにつき 1 回です。在庫の検索は 1 回、利用側は 2 つです。
  • 登録するものは何もありません。グラフは注釈「そのもの」です。

Check

「呼び出しごとに 1 回」を鵜呑みにしないでください。check_stockprint を入れて、Inspector から order_book を呼び出してみましょう。呼び出しごとに 1 行です。利用側は 2 つ、検索は 1 回です。

SDK がグラフを解析するのは、ツールが呼び出されたときではなく、登録されたときです。分類できないパラメーター(Context でも Resolve(...) でもツール引数の名前でもないもの)とリゾルバーの循環は、どちらも起動時に InvalidSignature を送出します。サーバーはクライアントが接続する前に失敗し、問題のパラメーターやリゾルバーの名前がエラーに示されます。

リゾルバーのパラメーターは、ツールのパラメーターとまったく同じように解決されます。別の Resolve(...)、名前で照合されるツール自身の引数、または Context です。ctx.headers もライフスパンのオブジェクトも、すべて使えます。

Warning

HTTP トランスポートでは、Contextctx.headers が含まれます。ヘッダーはツール引数と同じくクライアントが供給する入力です。ロケールや機能フラグには問題ありませんが、身元の確認には決して使わないでください。呼び出し側が誰であるかは、誰でも設定できるヘッダーではなく、認可レイヤー(認可)から得ます。

Tip

「呼び出しごとに 1 回」は文字どおりの意味です。次の tools/call では check_stock が再び実行されます。リクエストより長く生き続けるべきリソース(データベースプールや HTTP クライアントなど)は ライフスパン に置くものです。リゾルバーからは ctx.request_context.lifespan_context を通じて参照できます。

必要なときだけ尋ねる

リゾルバーは答えを知っている必要はありません。Elicit(message, Model) を返せば、SDK がユーザーに尋ねます。エリシテーション(elicitation) の仕組みを、代わりに実行してくれます。

server.py
from typing import Annotated

from pydantic import BaseModel, Field

from mcp.server import MCPServer
from mcp.server.mcpserver import Elicit, Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


class Backorder(BaseModel):
    confirm: bool = Field(description="Order anyway and wait?")


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


async def confirm_backorder(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
) -> Backorder | Elicit[Backorder]:
    if stock.copies > 0:
        return Backorder(confirm=True)  # in stock: nothing to ask
    return Elicit(f"{title!r} is out of stock (2-3 weeks). Order anyway?", Backorder)


@mcp.tool()
async def order_book(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
    backorder: Annotated[Backorder, Resolve(confirm_backorder)],
) -> str:
    """Order a book from the shop."""
    if not backorder.confirm:
        return "No order placed."
    if stock.copies == 0:
        return f"Backordered {title!r}; it ships in 2-3 weeks."
    return f"Ordered {title!r}."
  • 在庫がある場合:confirm_backorderBackorder を直接返します。質問もラウンドトリップもありません。ユーザーの作業を中断するのは、その答えが意味を持つときだけです。
  • 在庫がない場合:SDK がエリシテーションを送信し、答えを Backorder に照らして検証し、注入します。リゾルバーはプロトコルに一切触れません。
  • ツールは backorder.confirm をほかの引数と同じように読み取ります。いいえと答えるのも立派な答えです。エリシテーションは confirm=False で受理され、ツールは実行され、注文は行われません。尋ねることは、ツール本体の配管ではなく前提条件になりました。

では、ユーザーがまったく答えない場合、つまり質問を辞退したりキャンセルしたりした場合はどうなるでしょうか。

Check

Neuromancerorder_book を実行し、質問を辞退してみてください。注釈を Annotated[Backorder, Resolve(...)] と書いた場合、ツール本体は実行されません。呼び出しは、モデルが読めるエラー結果で失敗します。

Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline

前提条件としてはこれが正しいデフォルトです。答えがなければ注文もありません。辞退をツールで扱いたい結果にしたいとき(取り寄せはやめても、別のタイトルを提案したいなど)は、代わりに ElicitationResult[Backorder] で注釈を付けてください。ツールは受理・辞退・キャンセルの結果をまるごと受け取り、それに応じて分岐できます。この形式のほか、尋ねることに関するそれ以外のすべて(スキーマの規則、3 つの答え、会話のクライアント側)は エリシテーション で説明しています。

Info

フレームワークは、ネゴシエートされたプロトコルバージョンから質問のトランスポートを選びます。上のコードはどちらでも同じです。2026-07-28 以降では、質問はマルチラウンドトリップ(multi-round-trip)の tools/call の中で運ばれます。サーバーが質問を返し、クライアントの elicitation_callback がそれに答え、Client が呼び出しを再試行してくれます(マルチラウンドトリップリクエスト)。2025-11-25 以前では、呼び出しの途中で行われる同期的なエリシテーションリクエストです。各質問は 1 回の呼び出しにつきちょうど 1 回だけ尋ねられます。これは質問についての保証であり、リゾルバーについての保証ではありません。マルチラウンドトリップの形式では、質問の後に呼び出しが再開されるたびに、どのリゾルバーも再び実行される可能性があります。そのため、return Elicit(...) より前のコードはそれらのラウンドごとに実行されます。記録された答えは、繰り返される質問をユーザーに再度尋ねることなく満たします。記録された答えが参照されるのは、リゾルバーが尋ねたときだけです。check_stock のように尋ねずに答えるリゾルバーは、常に自分で計算した値を供給します。それぞれの答えは対応する質問に照合されるので、エリシテーションを行うリゾルバーは、ツールの引数とそれまでの答えから決定論的に質問を導かなければなりません。呼び出しごとに生成される値(default_factory の ID やタイムスタンプ)はラウンドごとに導き直されるため、答えを結び付けたい質問の中に含めてはいけません。そうした変わりやすいデータから組み立てた質問は、記録された答えをすべて古く見せてしまいます。その結果、サーバーはクライアントのラウンド上限が呼び出しを終わらせるまで、ラウンドごとに同じ質問を繰り返します。

ユーザーではなくクライアントに尋ねる

エリシテーションは、リゾルバーが尋ねられる 3 つの質問のうちの 1 つで、マルチラウンドトリップのフローではこれ以外は許されません。残りの 2 つはユーザーではなくクライアントに向けられます。Sample(...) を返せばクライアントを通じて LLM の呼び出しを実行し(sampling/createMessage リクエスト)、ListRoots() を返せばクライアントの現在のルート(roots)を取得します。どちらにも受理・辞退という結果はありません。利用側は結果の型を直接注釈に書きます。CreateMessageResult(リクエストが tools または tool_choice を伴う場合は CreateMessageResultWithTools)、または ListRootsResult です。

server.py
from typing import Annotated

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve, Sample
from mcp.types import CreateMessageResult, SamplingMessage, TextContent

mcp = MCPServer("Bookshop")


def suggest_title(genre: str) -> Sample:
    prompt = f"Suggest one {genre} book title. Answer with the title only."
    return Sample(
        [SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
        max_tokens=50,
    )


@mcp.tool()
async def recommend_book(
    genre: str,
    suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)],
) -> str:
    """Recommend a book in the given genre."""
    title = suggestion.content.text if suggestion.content.type == "text" else "the classics"
    return f"Today's {genre} pick: {title}"
  • フレームワークはこれらを Elicit とまったく同じように振り分けます。2026-07-28 ではマルチラウンドトリップの tools/call の中で、2025-11-25 では単独のサーバーからクライアントへのリクエストで運ばれます。宣言されていないケイパビリティは、-32021 のプロトコルエラーで呼び出しを拒否します(samplingroots、フォームモードの elicitation。リクエストが tools または tool_choice を伴う場合は sampling.tools)。
  • 上の info ボックスが質問について述べていることは、すべてそのまま当てはまります。Sample リクエストは、その正確な表現によって記録された結果と照合されます。そのため、ツールの引数とそれまでの答えから決定論的に組み立ててください。そうすれば、クライアントが LLM の呼び出しに支払うのはラウンドごとに 1 回ではなく、ツール呼び出しごとに 1 回になります。記録された結果は呼び出しの残りの間 request_state に載って運ばれるため、補完が非常に大きいと、残りのラウンドトリップがすべて重くなります。
  • 単独のサンプリングとルートの「機能」は、2026-07-28 で非推奨になります(SEP-2577)。クライアントのモデルを必要とする新しいサーバーは、この運び手を通じて尋ねます。必要としないサーバーは、LLM プロバイダーと直接統合してください。"none" 以外の include_context の値はそれ自体が非推奨です。使わないでください。

まとめ

  • ツールのパラメーターに Annotated[T, Resolve(fn)] を付けると、SDK が fn を実行し、その戻り値を注入します。
  • 解決されるパラメーターはモデルからは見えず、クライアントからも渡せません。モデルがでっち上げてはならない値(価格、身元、権限)はここに置きます。
  • リゾルバーのパラメーターも同じ方法で解決されます。Context、別の Resolve(...)、または名前で照合されるツール引数です。グラフは、利用側がいくつあっても、各リゾルバーをラウンドごとに最大 1 回だけ実行します。各質問はちょうど 1 回だけ尋ねられ、質問の後に呼び出しが再開されると、どのリゾルバーも再び実行される可能性があります。
  • 不正なグラフは、呼び出しの途中ではなく登録時に InvalidSignature で失敗します。
  • ユーザーに尋ねるには Elicit(message, Model) を返します。ただし、必要なときだけです。包まない注釈は辞退されると中断し、ElicitationResult[T] ならツールが分岐できます。
  • クライアントに LLM の補完やルートの一覧を尋ねるには、Sample(...) または ListRoots() を返します。そのままの結果が注入されます。

サーバーが起動時に一度だけ組み立てる状態と、ハンドラーからそこに到達する方法については、ライフスパン のページを参照してください。