Зависимости
Машинный перевод
Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.
Аргументы инструмента приходят от модели. Некоторые значения приходить от неё не должны никогда: цена, найденная в ваших записях; подтверждение, которое может дать только человек; всё, что модель способна исказить, просто выдумав.
Зависимости — это параметры, которые заполняют ваши собственные функции. Вы аннотируете параметр, указываете функцию, и SDK вызывает её до запуска инструмента.
Объявление зависимости
Оберните тип параметра в Annotated[...] и добавьте Resolve(fn):
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/list сообщает для reserve_book:
{
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"}
},
"required": ["title"],
"title": "reserve_bookArguments"
}
Одно свойство. Как и Context на странице Объект Context, разрешённый параметр — это договор между вами и SDK: stock нет в схеме, модели о нём никогда не сообщают, а значение stock, которое клиент всё же пришлёт, игнорируется. Значение резолвера — единственное, которое может получить инструмент.
В последнем и весь смысл. Параметр, который модель не может передать, — это параметр, в котором модель не может ошибиться.
Попробуйте сами
Запустите сервер с MCP Inspector:
uv run mcp dev server.py
В форме для reserve_book одно поле — title. Поля stock в ней нет нигде. Вызовите инструмент с Dune:
Reserved 'Dune' (6 copies left).
Тело инструмента ничего не искало: сначала выполнился check_stock, и возвращённый им Stock пришёл как аргумент. Попробуйте Neuromancer — и тот же резолвер передаст инструменту ноль.
Tip
Можно было бы просто вызвать check_stock(title) в теле инструмента. Объявляйте зависимость,
когда значение заслуживает большего, чем вызов вспомогательной функции: каждый инструмент,
которому нужны остатки, объявляет один и тот же параметр, а SDK запускает резолвер не более
одного раза за вызов, сколько бы потребителей его ни объявляли. Следующие разделы добавят
остальное: резолверы, зависящие друг от друга, и резолверы, которые спрашивают пользователя.
Зависимости зависимостей
Резолвер может объявлять собственные зависимости той же аннотацией:
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_deliveryзависит отcheck_stock. SDK выполняет граф по порядку: сначала остатки, затем оценка, затем инструмент.- И
stock, иdeliveryв конечном счёте нуждаются вcheck_stock, но он выполняется один раз за вызов. Один запрос к складу, два потребителя. - Регистрировать ничего не нужно. Граф — это и есть аннотации.
Check
Не принимайте «один раз за вызов» на веру. Поставьте print в check_stock и вызовите
order_book из Inspector: одна строка на вызов. Два потребителя, один поиск.
SDK анализирует граф при регистрации инструмента, а не при его вызове. Параметр, который не удаётся классифицировать (не Context, не Resolve(...), не имя аргумента инструмента), и цикл резолверов одинаково выбрасывают InvalidSignature при запуске. Сервер падает ещё до того, как подключится первый клиент, и в ошибке назван виновный параметр или резолвер.
Параметры резолвера разрешаются точно так же, как параметры инструмента: другой Resolve(...), собственные аргументы инструмента по имени или Context — ctx.headers, объект жизненного цикла (lifespan), всё это.
Warning
На HTTP-транспортах Context включает ctx.headers. Заголовки — это входные данные от
клиента, как любой аргумент инструмента: годятся для локали или флага функции, но никогда —
для установления личности. Кто именно вызывает, определяет слой авторизации
(Авторизация), а не заголовок, который может выставить кто угодно.
Tip
Один раз за вызов означает ровно это: следующий tools/call снова запустит check_stock.
Ресурсу, который должен пережить запрос (пул соединений с базой данных, HTTP-клиент), место
на странице Жизненный цикл, а резолвер может добраться до него через
ctx.request_context.lifespan_context.
Вопрос пользователю, когда без него нельзя
Резолвер не обязан знать ответ. Он может вернуть Elicit(message, Model), и SDK спросит пользователя — это механизм элицитации (elicitation) со страницы Элицитация, запущенный за вас:
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_backorderвозвращаетBackorderнапрямую. Нет вопроса — нет лишнего раунда обмена. Пользователя отвлекают только тогда, когда его ответ на что-то влияет. - Нет в наличии: SDK отправляет элицитацию, проверяет ответ по
Backorderи внедряет его. Резолвер вообще не касается протокола. - Инструмент читает
backorder.confirmкак любой другой аргумент. Ответ нет — тоже ответ: элицитация принимается сconfirm=False, инструмент выполняется, и заказ не оформляется. Вопрос стал предусловием, а не служебным кодом в теле инструмента.
А если пользователь вообще не станет отвечать — отклонит вопрос или отменит его?
Check
Запустите order_book для Neuromancer и отклоните вопрос. С аннотацией в виде
Annotated[Backorder, Resolve(...)] тело инструмента не выполняется вовсе; вызов завершается
результатом-ошибкой, который модель может прочитать:
Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline
Это правильное поведение по умолчанию для предусловия: нет ответа — нет заказа. Когда отказ — это исход, который инструмент хочет обработать (пропустить дозаказ, но всё же предложить другую книгу), укажите в аннотации ElicitationResult[Backorder], и инструмент получит полный исход accept/decline/cancel, по которому можно ветвиться. Эту форму, как и всё остальное о том, как спрашивать: правила схемы, три варианта ответа, сторону клиента в этом разговоре, — показывает страница Элицитация.
Info
Фреймворк выбирает транспорт для вопроса по согласованной версии протокола; приведённый выше
код одинаков в обоих случаях. На 2026-07-28 и новее вопрос передаётся внутри
многораундового (multi-round-trip) tools/call: сервер возвращает его, elicitation_callback
клиента отвечает, а Client повторяет вызов за вас (Многораундовые запросы).
На 2025-11-25 и старше это синхронный запрос элицитации посреди вызова. Каждый вопрос
задаётся ровно один раз за вызов — это гарантия о вопросе, а не о резолвере. В многораундовой
форме любой резолвер может выполниться снова всякий раз, когда вызов возобновляется после
вопроса, поэтому код перед return Elicit(...) выполняется в каждом таком раунде; записанный
ответ затем закрывает повторный вопрос, не спрашивая пользователя заново. К записанному ответу
обращаются только тогда, когда резолвер спрашивает; резолвер, который отвечает, не спрашивая,
как check_stock, всегда поставляет собственное вычисленное значение. Поскольку каждый ответ
сопоставляется со своим вопросом, резолвер с элицитацией должен выводить вопрос
детерминированно из аргументов инструмента и предыдущих ответов. Значение, генерируемое заново
при каждом вызове (идентификатор из default_factory, метка времени), пересчитывается в каждом
раунде и не должно попадать в вопрос, к которому привязывается ответ. Вопрос, построенный на
таких изменчивых данных, делает любой записанный ответ устаревшим на вид, и сервер задаёт его
заново в каждом раунде, пока лимит раундов на стороне клиента не завершит вызов.
Вопрос клиенту, а не пользователю
Элицитация — один из трёх вопросов, которые может задать резолвер, и многораундовый поток других не допускает. Два других адресованы клиенту, а не пользователю: верните Sample(...), чтобы выполнить вызов LLM через клиент (запрос sampling/createMessage), или ListRoots(), чтобы получить текущие корневые каталоги (roots) клиента. Ни у одного из них нет исхода accept/decline; потребитель аннотирует тип результата напрямую: CreateMessageResult (CreateMessageResultWithTools, когда запрос несёт tools или tool_choice) или ListRootsResult:
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: внутри многораундовогоtools/callна 2026-07-28, через отдельный запрос сервер->клиент на 2025-11-25. При необъявленной возможности вызов отклоняется с протокольной ошибкой-32021(sampling,roots,elicitationв режиме формы;sampling.tools, когда запрос несётtoolsилиtool_choice). - Всё, что сказано о вопросах в блоке info выше, применимо без изменений: запрос
Sampleсопоставляется с записанным результатом по точному представлению, поэтому стройте его детерминированно из аргументов инструмента и предыдущих ответов; тогда клиент платит за вызов LLM один раз за вызов инструмента, а не один раз за раунд. Записанный результат передаётся вrequest_stateдо конца вызова, так что очень большой результат генерации утяжеляет каждый оставшийся раунд обмена. - Отдельные возможности сэмплирования (sampling) и корневых каталогов объявлены устаревшими в 2026-07-28 (SEP-2577). Новые серверы, которым нужна модель клиента, спрашивают через этот носитель; серверам, которым она не нужна, следует интегрироваться с провайдером LLM напрямую. Значения
include_context, отличные от"none", сами объявлены устаревшими; избегайте их.
Итоги
Annotated[T, Resolve(fn)]у параметра инструмента: SDK запускаетfnи внедряет её возвращаемое значение.- Разрешённый параметр невидим для модели, и клиент не может его передать. Значениям, которые модель не должна выдумывать, — ценам, данным о личности, правам доступа — место здесь.
- Параметры резолвера разрешаются так же:
Context, другойResolve(...)или аргумент инструмента по имени. Граф запускает каждый резолвер не более одного раза за раунд, сколько бы потребителей у него ни было; каждый вопрос задаётся ровно один раз, и любой резолвер может выполниться снова, когда вызов возобновляется после вопроса. - Плохие графы падают при регистрации с
InvalidSignature, а не посреди вызова. - Возвращайте
Elicit(message, Model), чтобы спросить пользователя, — только когда иначе нельзя. Аннотации без обёртки прерывают вызов при отказе;ElicitationResult[T]позволяет инструменту ветвиться. - Возвращайте
Sample(...)илиListRoots(), чтобы запросить у клиента генерацию LLM или список корневых каталогов; внедряется сам результат.
Состоянию, которое сервер строит один раз при запуске, и тому, как обработчик до него добирается, посвящена страница Жизненный цикл.