Залежності
Машинний переклад
Цю сторінку перекладено автоматично з англомовної документації, і основною версією є англомовна сторінка. Якщо щось читається неправильно, на сторінці Переклади пояснено, як про це повідомити.
Аргументи інструмента надходять від моделі. Деякі значення звідти надходити не повинні ніколи: ціна, знайдена у ваших записах; підтвердження, яке може дати лише людина; усе, що модель могла б зіпсувати, якби вигадала сама.
Залежності — це параметри, які заповнюють ваші власні функції. Ви анотуєте параметр, указуєте функцію, а 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). - Усе, що сказано про запитання в інформаційному блоці вище, застосовується без змін: запит
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 або список кореневих каталогів; впроваджується сам результат.
Про стан, який сервер будує один раз під час запуску, і про те, як обробник до нього дістається, — сторінка Життєвий цикл.