Уведомления: логи и прогресс
Инструмент, который собирает отчёт по проекту, может работать минуту. Всё это время хост не знает, жив ли сервер, а пользователь смотрит на крутящийся индикатор. Уведомления решают эту проблему: сервер может по ходу дела сообщать «обработано 40 из 120 задач», не дожидаясь конца. К концу урока вы будете знать, чем уведомление отличается от запроса, как работает прогресс, что такое протокольное логирование, и какое из этого стоит применять сегодня.
Что такое уведомление
В JSON-RPC 2.0 есть три вида сообщений: запрос, ответ и уведомление. Уведомление — это запрос без id. Раз нет id, ответить на него невозможно, и получатель не должен пытаться. Это «выстрелил и забыл»:
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": { "progressToken": "job-17", "progress": 40, "total": 120 }
}Уведомления ходят в обе стороны, но в этом уроке нас интересуют те, что идут от сервера к клиенту. В ревизии 2026-07-28 они делятся на два класса. Уведомления, привязанные к запросу, — прогресс и логи — доставляются на том же потоке, что и ответ на этот запрос, и только до него. Уведомления об изменениях — «список инструментов изменился», «ресурс обновился» — доставляются на отдельном долгоживущем потоке, который клиент открывает запросом subscriptions/listen. Мы сосредоточимся на первом классе.
Прогресс
Прогресс — единственное уведомление, которое клиент должен явно запросить. Если хост хочет получать обновления по конкретному вызову, он кладёт в запрос токен:
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "build_report",
"arguments": { "sprint": "2026-09" },
"_meta": { "progressToken": "job-17" }
}
}_meta — поле для метаданных, которое есть у любого запроса. Токен выбирает клиент; он должен быть строкой или числом и уникальным среди активных запросов. Дальше сервер может (но не обязан) слать уведомления с этим токеном:
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "job-17",
"progress": 40,
"total": 120,
"message": "Собираю задачи спринта"
}
}Правила простые. progress обязан расти от уведомления к уведомлению, даже если total неизвестен. total необязателен — когда сервер не знает, сколько всего работы, он его не указывает. Оба числа могут быть дробными. message — человекочитаемая строка для интерфейса. После завершения запроса уведомления с его токеном должны прекратиться. И обе стороны должны ограничивать частоту: уведомление на каждую из десяти тысяч строк — это не прогресс, а флуд.
Есть ещё одно тонкое свойство. Спецификация разрешает клиенту сбрасывать таймаут запроса при получении прогресса: раз пришло уведомление, сервер работает. Но общий максимальный таймаут клиент всё равно обязан соблюдать. Так что прогресс — это ещё и способ не быть убитым за медленную работу, но не индульгенция на бесконечную.
Прогресс в SDK
Серверу нужен объект Context. Объявите параметр с такой аннотацией в инструменте — SDK найдёт его по типу и подставит; в схему инструмента он не попадёт:
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("reports")
@mcp.tool()
async def build_report(sprint: str, ctx: Context) -> str:
"""Собрать отчёт по спринту."""
tasks = await load_tasks(sprint)
total = len(tasks)
for i, task in enumerate(tasks, start=1):
await analyze(task)
await ctx.report_progress(i, total, f"Задача {task.key}")
return render(tasks)report_progress(progress, total=None, message=None) — единственный метод, который вам нужен. Если клиент не прислал токен, вызов ничего не делает, так что проверять это самостоятельно не нужно.
На стороне клиента прогресс запрашивается для каждого вызова отдельно — колбэком в call_tool:
async def on_progress(progress: float, total: float | None, message: str | None) -> None:
print(f"{progress}/{total}: {message}")
result = await client.call_tool("build_report", {"sprint": "2026-09"}, progress_callback=on_progress)SDK сам сгенерирует токен и положит его в _meta.
Логи через протокол
Второй вид уведомлений — notifications/message. Сервер отправляет сообщение с уровнем важности, необязательным именем логгера и произвольными JSON-данными:
{
"jsonrpc": "2.0",
"method": "notifications/message",
"params": {
"level": "warning",
"logger": "tracker",
"data": { "message": "Трекер отвечает медленно", "latency_ms": 4200 }
}
}Уровни взяты из syslog: debug, info, notice, warning, error, critical, alert, emergency. Как клиент фильтрует, зависит от ревизии. В 2025-11-25 клиент один раз отправлял запрос logging/setLevel, и сервер дальше слал всё, что не ниже уровня. В 2026-07-28 фильтр стал частью запроса: клиент кладёт io.modelcontextprotocol/logLevel в _meta конкретного запроса, и только для этого запроса сервер может слать логи — на том же потоке, до ответа. Нет ключа в _meta — нет логов.
Вот что важно понимать: эти логи никогда не попадают в модель. Их получает клиент и может показать в панели, сохранить или проигнорировать. Модель видит только то, что инструмент вернул.
В SDK v2 методы ctx.info(), ctx.debug(), ctx.warning(), ctx.error() и общий ctx.log(level, data, logger_name=...) сохранились, но помечены как устаревшие и при вызове выдают MCPDeprecationWarning. На стороне клиента их принимает logging_callback, причём на современном протоколе нужно ещё указать log_level в конструкторе Client, иначе клиент ничего не запросит и ничего не получит.
Почему логирование устарело, а прогресс нет
С ревизии 2026-07-28 логирование через протокол помечено устаревшим (SEP-2577). Рекомендованная замена — обычный logging Python: в stdio-сервере он пишет в stderr, который хост может собрать, а для продакшена — OpenTelemetry. Логика такая: логи нужны оператору сервера, а не пользователю хоста, и тащить их через протокол в хост — не та точка назначения. Ещё одна причина: на HTTP-транспорте с несколькими экземплярами сервера «канал к клиенту» перестал быть чем-то определённым.
Прогресс — другое дело. Он нужен именно пользователю хоста, привязан к конкретному запросу и не требует от сервера ничего помнить. Поэтому он остался и остаётся основным способом показать, что долгая операция идёт.
Практический вывод для нового сервера: ctx.report_progress() для прогресса, logging.getLogger(__name__) для логов. Методы ctx.info() и его братья — только если вы сознательно поддерживаете старые хосты и вам нужно, чтобы сообщения были видны в их интерфейсе.
Уведомления об изменениях
Ради полноты — второй класс. Когда у сервера меняется набор инструментов (например, после логина пользователя стали доступны новые), он может сообщить об этом клиенту уведомлением notifications/tools/list_changed, и клиент перезапросит tools/list. Есть аналоги для промптов и ресурсов, а также notifications/resources/updated для конкретного ресурса.
В 2026-07-28 такие уведомления не приходят сами: клиент открывает подписку запросом subscriptions/listen с фильтром (toolsListChanged: true и так далее), сервер подтверждает её и дальше шлёт только запрошенное. В SDK на стороне сервера это await ctx.notify_tools_changed(). Нам это понадобится в уроке про состояние, когда речь пойдёт о нескольких экземплярах сервера.
Попробуйте сами
10–15 мин на рабочем месте- Найдите в своём сервере самый долгий инструмент. Разбейте его работу на этапы и напишите, какое сообщение и какие числа
progress/totalвы бы отправили на каждом. Если общее число неизвестно заранее, решите, что считать «прогрессом», который обязан расти. - Прочитайте раздел Progress спецификации и найдите, что должно произойти с уведомлениями после того, как запрос завершён.
- Замените в своём сервере все
printнаlogging.getLogger(__name__)и запустите его под Inspector: убедитесь, что сообщения появляются в stderr и не ломают протокол.
Коротко
- Уведомление — JSON-RPC-сообщение без
id; на него не отвечают. - Прогресс включает клиент:
progressTokenв_metaзапроса; сервер шлётnotifications/progressс растущимprogress, необязательнымtotalиmessage. - В SDK:
ctx.report_progress(progress, total, message)на сервере,progress_callbackвcall_toolна клиенте. - Логи через протокол —
notifications/messageс уровнями syslog; модель их не видит. - Логирование через протокол устарело с
2026-07-28: используйтеloggingв stderr; прогресс остаётся. - Уведомления об изменениях списков в
2026-07-28доставляются по подпискеsubscriptions/listen.
Видеоверсия
Сценарий озвучки · 496 слов, ≈ 4 мин
Представьте инструмент, который собирает отчёт по спринту. Он ходит в трекер, анализирует сто двадцать задач и работает минуту. Всё это время хост не знает, жив ли сервер, а пользователь смотрит на крутящийся кружок. Сегодня разберём, как сервер может рассказывать о ходе работы, не дожидаясь конца.
Для начала — что такое уведомление. В протоколе три вида сообщений: запрос, ответ и уведомление. Уведомление — это запрос без идентификатора. Раз нет идентификатора, ответить на него нельзя, и получатель не должен пытаться. Выстрелил и забыл. В последней ревизии протокола уведомления от сервера делятся на два класса. Первые привязаны к конкретному запросу — это прогресс и логи, они приходят на том же потоке, что и ответ, и только до него. Вторые — про изменения: список инструментов поменялся, ресурс обновился. Их клиент получает по отдельной подписке.
Начнём с прогресса. Он устроен так: клиент, который хочет получать обновления, кладёт в запрос токен прогресса. Токен — строка или число, уникальное среди активных запросов. Дальше сервер может отправлять уведомления с этим токеном: текущее значение, необязательное общее значение и человекочитаемое сообщение. Правила простые. Значение прогресса обязано расти с каждым уведомлением. Общее значение можно не указывать, если оно неизвестно. После завершения запроса уведомления должны прекратиться. И нужно ограничивать частоту: уведомление на каждую строку лога — это не прогресс, а спам. Есть ещё тонкость: клиент может сбрасывать таймаут запроса при получении прогресса, потому что сервер явно работает. Но общий максимум он всё равно соблюдает.
В Python SDK прогресс — это один метод. Вы объявляете в инструменте параметр с типом Context, SDK подставит его сам, в схему инструмента он не попадёт. И дальше вызываете report_progress с текущим значением, общим и сообщением. Если клиент токен не прислал, вызов ничего не делает. На стороне клиента прогресс запрашивается на каждый вызов отдельно: передаёте колбэк в вызов инструмента, SDK сам сгенерирует токен.
Второй вид уведомлений — логи через протокол. Сервер отправляет сообщение с уровнем важности из syslog, от debug до emergency, именем логгера и произвольными данными. Важно понимать: эти логи никогда не попадают в модель. Их получает клиент и решает, что с ними делать. Модель видит только то, что инструмент вернул. В SDK методы info, warning, error на контексте остались, но помечены как устаревшие и выдают предупреждение.
Почему логирование устарело, а прогресс — нет? Логи нужны оператору сервера, а не пользователю хоста. Тащить их через протокол в хост — не та точка назначения. Рекомендованная замена — обычный логгер Python, который в сервере на стандартном вводе-выводе пишет в поток ошибок, а в продакшене — OpenTelemetry. А прогресс нужен именно пользователю хоста, привязан к конкретному запросу и не требует от сервера ничего помнить. Поэтому он остался. Практический вывод: для нового сервера — report_progress для прогресса и стандартный логгер для логов.
Коротко про второй класс уведомлений. Когда у сервера меняется набор инструментов, он может сообщить об этом, и клиент перезапросит список. В новой ревизии такие уведомления не приходят сами: клиент открывает подписку с фильтром, а сервер шлёт только запрошенное. Это нам понадобится, когда будем говорить о нескольких экземплярах сервера.
В следующем уроке построим сервер отчётов с прогрессом и посмотрим, как уведомления приходят на клиент по мере работы.
