Определяем ресурсы
У нашего сервера есть инструменты, и модель умеет читать документы сама. Но в уроке про примитивы мы говорили, что есть второй путь: человек заранее прикладывает документ к вопросу, без решения модели и без подтверждения. Это ресурсы. В этом уроке добавим их на сервер, а в следующем — научим клиент ими пользоваться.
Прямой ресурс
Добавьте в docs_server.py:
@mcp.resource("docs://list", mime_type="application/json")
def list_docs() -> list[str]:
"""Список документов проекта."""
return list(DOCS)Декоратор @mcp.resource() принимает URI ресурса первым аргументом. Схема docs:// — наша собственная; протокол не ограничивает схемы, и для локальных данных принято придумывать говорящие: docs://, tickets://, config://. Для настоящих файлов используют file:///путь.
Функция возвращает список — SDK сериализует его в JSON и отдаст как текст. Отсюда mime_type="application/json": клиент должен знать, что это не просто строка, а данные, которые можно разобрать. По умолчанию тип text/plain. Строку функция может вернуть как есть, bytes уйдут в base64, словарь и модель Pydantic — в JSON.
Описание, как и у инструментов, берётся из docstring, имя — из имени функции. Здесь они предназначены не модели, а человеку и хосту: именно их покажет список ресурсов в интерфейсе.
Шаблон ресурса
Список — это хорошо, но нужен доступ к каждому документу. Заводить по ресурсу на файл вручную нельзя: файлов может быть сколько угодно. Для этого есть шаблоны:
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
@mcp.resource("docs://{name}", mime_type="text/markdown")
def get_doc(name: str) -> str:
"""Содержимое документа проекта."""
if name not in DOCS:
raise ResourceNotFoundError(f"Документа {name!r} нет.")
return DOCS[name]{name} в URI — параметр. Он должен точно совпадать с именем аргумента функции, иначе SDK откажется регистрировать ресурс ещё при импорте. Когда клиент запросит docs://risks.md, SDK сопоставит его с шаблоном, вытащит name = "risks.md" и вызовет функцию.
В протоколе прямые ресурсы и шаблоны живут в разных списках: resources/list вернёт docs://list, а resources/templates/list — docs://{name}. Чтение в обоих случаях делается одним методом resources/read с конкретным URI. Хост, получив шаблон, может показать пользователю поле для ввода параметра или подставить значения из контекста.
Обратите внимание: docs://list и шаблон docs://{name} могли бы конфликтовать — list формально подходит под {name}. SDK сначала проверяет точные совпадения, поэтому прямой ресурс побеждает. Но лучше не создавать таких пересечений в настоящих серверах: назовите список docs://index или docs:///, если документ с именем list возможен.
Ошибки
Для ресурсов нет аналога ToolError с флагом is_error: чтение либо удаётся, либо нет. ResourceNotFoundError превращается в ошибку протокола с кодом -32602 и URI в данных ошибки, и клиент получит исключение. Это осмысленно: ресурс читает приложение, а не модель, и «исправлять» тут некому — хост покажет ошибку пользователю.
Из этого следует правило: не давайте ресурсам падать по своим причинам. Если данные недоступны, поднимайте ResourceNotFoundError (или общий ResourceError из того же модуля), а не KeyError.
Ресурс или инструмент?
У нас теперь два пути к содержимому документа: инструмент read_doc и ресурс docs://{name}. Это не дублирование, а два сценария.
Инструмент вызывает модель, когда по ходу задачи понимает, что ей нужен документ. «Проверь, согласуется ли оценка с брифом» — модель сама вызовет read_doc дважды. Каждый вызов — решение модели, подтверждение хоста, отдельный шаг в цикле.
Ресурс подключает человек или приложение до того, как модель что-то решила. «Вот бриф, вот риски — что упущено?» — пользователь прикладывает два документа, и модель получает их в первом же сообщении. Ни решений, ни подтверждений, ни лишних шагов.
Практическое правило: если данные «пассивные» и их часто прикладывают к вопросу целиком — это ресурс. Если получение данных — действие с параметрами, которые модель должна подобрать, — это инструмент. Списки, справочники, схемы, конфигурации почти всегда ресурсы. Поиск, фильтрация, запросы к API — инструменты.
Ещё один довод за ресурсы: их перечисление ничего не стоит. При resources/list функции не выполняются — SDK отдаёт только метаданные. Функция запускается только при чтении конкретного URI.
Проверяем
Дополните check.py:
for res in (await client.list_resources()).resources:
print("ресурс:", res.uri, res.mime_type, "—", res.description)
for tpl in (await client.list_resource_templates()).resource_templates:
print("шаблон:", tpl.uri_template, tpl.mime_type, "—", tpl.description)
listing = await client.read_resource("docs://list")
print("docs://list ->", listing.contents[0].text)
doc = await client.read_resource("docs://risks.md")
print("docs://risks.md ->", doc.contents[0].mime_type, doc.contents[0].text[:40])
try:
await client.read_resource("docs://nope.md")
except Exception as e:
print("нет документа ->", type(e).__name__, e)read_resource возвращает объект с полем contents — список блоков (ресурс может состоять из нескольких частей). У текстового блока есть text и mime_type, у бинарного — blob. В выводе вы увидите JSON-список для docs://list, начало документа с типом text/markdown и исключение MCPError для несуществующего имени.
Теперь откройте инспектор (uv run mcp dev docs_server.py): во вкладке Resources появятся и прямой ресурс, и шаблон, а рядом с шаблоном — поле для параметра. Прочитайте docs://brief.md и найдите запрос resources/read во вкладке Protocol.
Попробуйте сами
10–15 мин на рабочем месте- Добавьте ресурс
docs://statsс типомapplication/json, который возвращает словарь: число документов и общий объём в символах. Прочитайте его вcheck.pyи разберите JSON черезjson.loads. - Если вы перевели
DOCSна настоящую папку, замените схему наfile://и возвращайте абсолютный путь в имени ресурса. Проверьте, что инспектор показывает список. - Подумайте, какие ресурсы стоило бы отдать серверу вашего трекера задач: список проектов? Схему полей задачи? Текущий спринт? Для каждого решите — прямой ресурс или шаблон.
Коротко
@mcp.resource("схема://путь")регистрирует прямой ресурс;{параметр}в URI делает его шаблоном.- Имя параметра в URI должно совпадать с аргументом функции; проверяется при импорте.
mime_typeговорит клиенту, что за данные; списки и словари уходят как JSON.- Ошибки чтения —
ResourceNotFoundError, а неis_error: ресурс читает приложение, исправлять некому. - Ресурс — для данных, которые прикладывают к вопросу заранее; инструмент — для действий, параметры которых подбирает модель.
- Перечисление ресурсов бесплатно: функции выполняются только при чтении.
Видеоверсия
Сценарий озвучки · 495 слов, ≈ 4 мин
У сервера есть инструменты, и модель умеет читать документы сама. Но мы говорили, что есть второй путь: человек заранее прикладывает документ к вопросу, без решения модели и без подтверждения. Это ресурсы. Сегодня добавим их на сервер.
Первый ресурс — список документов. Декоратор «ресурс» принимает адрес: схема «докс», две косые черты, слово «лист». Схема наша собственная: протокол их не ограничивает, и для локальных данных принято придумывать говорящие. Функция возвращает список, SDK превращает его в джейсон, и мы указываем тип содержимого «application json», чтобы клиент знал: это данные, а не просто строка. Описание берётся из докстринга, но предназначено уже не модели, а человеку и хосту — именно его покажет список ресурсов в интерфейсе.
Второй ресурс — содержимое документа по имени. Заводить по ресурсу на файл вручную нельзя, файлов может быть сколько угодно. Для этого есть шаблоны: в адресе в фигурных скобках пишем имя параметра, и оно должно точно совпадать с аргументом функции. Когда клиент запросит документ «риски», SDK сопоставит адрес с шаблоном, вытащит имя и вызовет функцию. В протоколе прямые ресурсы и шаблоны живут в разных списках, а читаются одним методом.
Об ошибках. У ресурсов нет флага ошибки, как у инструментов: чтение либо удаётся, либо нет. Если документа нет, поднимаем специальное исключение «ресурс не найден», и клиент получит ошибку протокола. Это осмысленно: ресурс читает приложение, а не модель, исправлять некому — хост просто покажет ошибку пользователю.
Теперь главный вопрос: зачем два пути к одному документу — инструмент и ресурс? Это разные сценарии. Инструмент вызывает модель, когда по ходу задачи понимает, что ей нужен документ. «Проверь, согласуется ли оценка с брифом» — модель сама прочитает оба. Каждый вызов — её решение, подтверждение хоста, отдельный шаг. Ресурс подключает человек до того, как модель что-то решила. «Вот бриф, вот риски — что упущено?» — пользователь прикладывает документы, и модель получает их в первом же сообщении. Ни решений, ни подтверждений.
Практическое правило: если данные пассивные и их часто прикладывают целиком — это ресурс. Если получение данных — действие с параметрами, которые модель должна подобрать, — это инструмент. Списки, справочники, схемы — ресурсы. Поиск, фильтрация, запросы к API — инструменты. И ещё довод: перечисление ресурсов ничего не стоит, функции выполняются только при чтении.
Проверяем из клиента: печатаем список ресурсов и шаблонов, читаем список документов, читаем один документ, пробуем несуществующий и ловим исключение. Потом открываем инспектор: во вкладке ресурсов появились и прямой ресурс, и шаблон с полем для параметра.
Небольшое предупреждение о конфликте адресов. У нас есть прямой ресурс «список» и шаблон «документ по имени», и формально слово «список» подходит под шаблон. SDK сначала проверяет точные совпадения, поэтому прямой ресурс побеждает. Но в настоящих серверах таких пересечений лучше не создавать: назовите список иначе, если документ с таким именем возможен.
Задание для самостоятельной работы: добавьте ресурс со статистикой — число документов и общий объём — в формате джейсон, прочитайте его из клиента и разберите. А потом подумайте, какие ресурсы стоило бы отдать серверу вашего трекера задач: список проектов, схему полей, текущий спринт. Для каждого решите, прямой это ресурс или шаблон.
В следующем уроке научим наш клиент прикладывать ресурсы к вопросу — так, как это делает Claude Code через упоминание с собачкой.
