Ход выполнения
Машинный перевод
Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.
Инструмент, который работает тридцать секунд и все тридцать секунд молчит, выглядит сломанным.
Уведомления о ходе выполнения решают эту проблему. Инструмент сообщает, насколько он продвинулся, а клиент решает, что из этого нарисовать: полосу, спиннер, строку в логе.
Отчёт о ходе из инструмента
Примите параметр Context и вызовите report_progress:
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Bookshop")
@mcp.tool()
async def import_catalog(urls: list[str], ctx: Context) -> str:
"""Import book records from a list of catalog URLs."""
for done, url in enumerate(urls, start=1):
await ctx.report_progress(done, total=len(urls), message=f"Imported {url}")
return f"Imported {len(urls)} records."
Три аргумента, и их смысл определяете вы:
progress: насколько вы продвинулись. Спецификация требует, чтобы значение росло с каждым отчётом: никогда не повторяйте значение и не уменьшайте его.total: сколько всего, если это известно. Необязательный.message: одна понятная человеку строка об этом шаге. Необязательный.
ctx внедряется по аннотации типов, и модель его не видит: во входной схеме import_catalog единственное свойство — urls. Этому объекту целиком посвящена страница Объект Context; ход выполнения — одна из возможностей, которые он даёт.
Приём на стороне клиента
Клиент подписывается на каждый вызов отдельно, передавая progress_callback= в call_tool:
import anyio
from mcp import Client
from server import mcp
async def show(progress: float, total: float | None, message: str | None) -> None:
print(f"{message} ({progress}/{total})")
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool(
"import_catalog",
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
progress_callback=show,
)
print(result.structured_content)
anyio.run(main)
Колбэк — это async-функция, принимающая ровно то, что сообщил сервер: progress, total, message.
Info
Client(mcp) подключается напрямую к объекту сервера, в памяти, — это тот же клиент, на котором
построена страница Тестирование. Параметр progress_callback один
и тот же, какой бы транспорт ни использовал Client; а вот временны́е характеристики, которые вы
сейчас увидите, относятся к подключению в памяти. Оно выполняет колбэк прямо на месте, поэтому
каждый отчёт приходит до того, как call_tool вернёт управление. По настоящему транспорту
уведомления соревнуются с результатом, и медленный колбэк может всё ещё работать после того, как
call_tool уже вернул управление.
Попробуйте сами
Положите client.py рядом с server.py и запустите:
python client.py
Imported https://example.com/a.json (1/2)
Imported https://example.com/b.json (2/2)
{'result': 'Imported 2 records.'}
Каждый await ctx.report_progress(...) на сервере превратился в один вызов show на клиенте, в том же порядке, и обе строки напечатались до того, как call_tool вернул управление. Ход выполнения не упаковывается в результат: он передаётся потоком, пока инструмент ещё работает.
Warning
progress_callback относится к вызову, а не к Client. Аргумента конструктора для него нет,
потому что разным вызовам нужны разные колбэки: один управляет полосой загрузки, следующий —
строкой в логе.
Check
Теперь удалите progress_callback=show и запустите снова:
{'result': 'Imported 2 records.'}
Ни ошибки, ни предупреждения, тот же результат. report_progress ничего не делает, если
вызывающая сторона не запросила ход выполнения, поэтому сообщайте о нём безусловно и никогда
не гадайте, слушает ли кто-нибудь.
Когда общий объём неизвестен
total нужен, когда известен знаменатель. Часто это не так: вы вычитываете ленту, идёте по курсору, скачиваете что-то без заголовка длины.
Просто не указывайте его:
from collections.abc import AsyncIterator
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Bookshop")
async def fetch_records(feed_url: str) -> AsyncIterator[str]:
for title in ("Dune", "Neuromancer", "Hyperion"):
yield f"{feed_url}#{title}"
@mcp.tool()
async def import_feed(feed_url: str, ctx: Context) -> str:
"""Import every record a catalog feed yields."""
imported = 0
async for record in fetch_records(feed_url):
imported += 1
await ctx.report_progress(imported, message=f"Imported {record}")
return f"Imported {imported} records."
Колбэк получает total=None. Клиент по-прежнему может показывать активность («пока импортировано 3...»), но не процент. Не выдумывайте общий объём ради полосы покрасивее.
Tip
progress не обязан считать что-то конкретное. Байты, строки, страницы: выберите единицу,
понятную пользователю, и обещайте total, только если сможете это обещание сдержать.
Итоги
await ctx.report_progress(progress, total=None, message=None)из любого инструмента, принимающегоContext.- Клиент передаёт
progress_callback=вcall_tool: на каждый вызов, никогда не вClient. - Колбэк имеет вид
async (progress, total, message) -> Noneи срабатывает, пока инструмент ещё работает. - Нет колбэка у вызова —
report_progressничего не делает. Сообщайте безусловно. - Опускайте
total, когда он неизвестен; колбэк получитNone.
Ход выполнения — это то, что работающий инструмент показывает пользователю. Строки, которые он пишет в лог для вас, человека, обслуживающего сервер, — это другой канал: Логирование.