Доступ к ресурсам
На сервере ресурсы есть, теперь нужно научить клиент ими пользоваться. Напомним, что ресурсы контролирует приложение: модель их не вызывает, она получает содержимое уже в запросе. Значит, клиент должен решить, какие ресурсы приложить, прочитать их и вставить в сообщение. В этом уроке сделаем это через упоминания @имя — так же, как работает @сервер:ресурс в Claude Code.
Перечислить и прочитать
Два метода клиента, которые нам нужны, вы уже видели в check.py:
listing = await client.list_resources() # прямые ресурсы
templates = await client.list_resource_templates() # шаблоны
doc = await client.read_resource("docs://brief.md")read_resource возвращает объект с полем contents — список блоков. Для текстовых ресурсов это TextResourceContents с полями uri, mime_type и text; для бинарных — BlobResourceContents с blob в base64. Один ресурс обычно состоит из одного блока, но протокол разрешает несколько.
Обратите внимание на тип: это TextResourceContents, а не TextContent, который мы использовали для результатов инструментов. Разные примитивы — разные типы блоков. Перепутать легко, и тогда фильтр по isinstance молча отбросит всё содержимое.
Упоминания в запросе
Договоримся о синтаксисе: слово, начинающееся с @, — имя документа, который нужно приложить. «Сравни @brief.md и @risks.md — что не учтено?» должно превратиться в сообщение, где перед вопросом стоят оба документа.
Добавьте в client.py:
from mcp_types import TextContent, TextResourceContents
async def attach_docs(client: Client, query: str) -> str:
"""Заменить упоминания @имя на содержимое ресурсов docs://имя."""
attachments = []
for word in query.split():
if not word.startswith("@"):
continue
name = word[1:].strip(",.;:!?")
resource = await client.read_resource(f"docs://{name}")
text = "\n".join(c.text for c in resource.contents if isinstance(c, TextResourceContents))
attachments.append(f"<document name=\"{name}\">\n{text}\n</document>")
if not attachments:
return query
return "\n\n".join(attachments) + "\n\n" + queryИ измените вызов в main:
try:
content = await attach_docs(client, query)
print(await run(client, [{"role": "user", "content": content}]))
except Exception as e:
print("Ошибка:", e)Разберём. Для каждого @слова мы собираем URI по шаблону docs://{name} и читаем ресурс. Содержимое оборачиваем в тег <document name="..."> — это не требование протокола, а приём для модели: так она видит, где кончается один документ и начинается другой, и может ссылаться на них по имени. Все вложения ставим перед вопросом: модели удобнее сначала прочитать материалы, потом задание.
Если документа нет, read_resource бросит MCPError, и except в main покажет её пользователю. Модель до этого не дойдёт — и правильно: ошибку в имени файла должен исправлять человек, а не модель.
Запуск
uv run client.pyСпросите: «Сравни @brief.md и @risks.md — какие риски не следуют из брифа?». В stderr не появится ни одного [вызов ...]: модель получила оба документа сразу и ответила без инструментов. Теперь спросите то же самое без @: «Сравни бриф и риски». Появятся два вызова read_doc. Тот же результат, но два лишних шага, два решения модели и — в настоящем хосте — два подтверждения.
Это и есть разница между примитивами в действии. Когда вы знаете, что нужно модели, ресурс дешевле. Когда не знаете — инструменты позволяют ей разобраться самой.
Как это делает Claude Code
В Claude Code синтаксис почти такой же, только с именем сервера: @project-docs:docs://brief.md. Хост делает ровно то, что мы написали: находит клиент нужного сервера, вызывает resources/read, вставляет содержимое в сообщение. Ещё он умеет показать список доступных ресурсов при наборе @ — для этого и нужен resources/list: перечисление бесплатно, а пользователь видит, что можно приложить.
У нас список ресурсов пока не используется. Хорошее упражнение — вывести его при старте, как мы выводим инструменты, и подсказывать имена при вводе.
Что ещё может делать хост с ресурсами
Мы вставили ресурс целиком. Протокол этого не требует; приложение решает само:
- Целиком — как у нас. Подходит для документов разумного размера.
- Фрагмент — найти нужный кусок по ключевым словам или эмбеддингам и приложить только его. Так поступают хосты с большими базами знаний.
- Автоматически — приложить ресурс без упоминания, если он подходит по контексту: например, всегда прикладывать
docs://list, чтобы модель знала, какие документы существуют. - Как документ API — Anthropic API умеет принимать содержимое в блоке
documentс метаданными и цитированием. Для текстовых ресурсов это даёт модели ссылки на источник.
MIME-тип ресурса помогает выбрать способ: text/markdown можно вставить как есть, application/json — разобрать и показать таблицей, image/png — отправить как картинку.
Попробуйте сами
10–15 мин на рабочем месте- Выведите список ресурсов и шаблонов при старте клиента, рядом со списком инструментов. Используйте
uri_templateдля шаблонов, чтобы пользователь видел синтаксис. - Сделайте так, чтобы
docs://listприкладывался к каждому запросу автоматически, в отдельном теге<available-documents>. Проверьте, что модель стала реже ошибаться в именах при вызовеread_doc. - Замените для
application/jsonресурсов обёртку<document>на разобранный и заново отформатированный JSON (json.dumps(..., ensure_ascii=False, indent=2)). Подумайте, когда это полезно, а когда лишнее.
Коротко
list_resources,list_resource_templates,read_resource(uri)— три метода клиента для ресурсов.- Содержимое приходит в
contentsкакTextResourceContents(text) илиBlobResourceContents(blob) — не путать сTextContentинструментов. - Упоминание
@имя→ URI по шаблону → чтение → вставка в сообщение перед вопросом. - Ресурс даёт модели данные без вызовов, решений и подтверждений; инструмент — когда модель должна найти данные сама.
- Ошибка чтения ресурса — для пользователя, а не для модели.
- Хост сам решает, как приложить ресурс: целиком, фрагментом, автоматически.
Видеоверсия
Сценарий озвучки · 441 слово, ≈ 3 мин
На сервере ресурсы есть, теперь научим клиент ими пользоваться. Напомню: ресурсы контролирует приложение. Модель их не вызывает, она получает содержимое уже в запросе. Значит, клиент должен сам решить, что приложить, прочитать и вставить в сообщение. Сделаем это через упоминания с собачкой — так же, как работает Claude Code.
Клиенту нужны два метода: перечислить ресурсы и прочитать ресурс по адресу. Чтение возвращает список блоков, у текстового блока есть адрес, тип содержимого и текст. Здесь есть ловушка: тип блока у ресурсов называется иначе, чем у результатов инструментов. Разные примитивы — разные типы. Перепутать легко, и тогда фильтр молча отбросит всё содержимое, а вы будете долго искать, почему модель не видит документ.
Договоримся о синтаксисе: слово, начинающееся с собачки, — имя документа, который нужно приложить. Функция проходит по словам запроса, для каждого такого слова собирает адрес по шаблону «докс, две косые, имя», читает ресурс и оборачивает содержимое в тег «документ» с именем. Тег — не требование протокола, а приём для модели: так она видит, где кончается один документ и начинается другой. Все вложения ставим перед вопросом: модели удобнее сначала прочитать материалы, потом задание. Если документа нет, чтение бросит исключение, и пользователь увидит ошибку. Модель до этого не дойдёт — и правильно: опечатку в имени файла должен исправлять человек.
Запускаем и спрашиваем: «Сравни бриф и риски — какие риски не следуют из брифа?», указав оба документа через собачку. В потоке ошибок не появится ни одного вызова инструмента: модель получила документы сразу и ответила. Теперь спросим то же самое без собачек. Появятся два вызова чтения. Результат тот же, но два лишних шага, два решения модели и, в настоящем хосте, два подтверждения. Это разница между примитивами в действии: когда вы знаете, что нужно модели, ресурс дешевле; когда не знаете — инструменты позволяют ей разобраться самой.
Claude Code делает ровно то же самое, только с именем сервера перед адресом. Находит клиент нужного сервера, читает ресурс, вставляет содержимое. И ещё показывает список ресурсов при наборе собачки — для этого и нужно перечисление: оно бесплатно, а пользователь видит, что можно приложить.
Мы вставили ресурс целиком, но протокол этого не требует. Приложение решает само: приложить целиком, найти и приложить только нужный фрагмент, приложить автоматически без упоминания — например, всегда добавлять список документов, чтобы модель знала, что существует. Тип содержимого подсказывает способ: маркдаун вставить как есть, джейсон разобрать, картинку отправить как картинку.
У нас список ресурсов пока не используется. Хорошее упражнение — вывести его при старте клиента рядом со списком инструментов, чтобы пользователь видел, что можно приложить. Второе упражнение — прикладывать список документов к каждому запросу автоматически, в отдельном теге. Тогда модель будет знать, какие документы существуют, и реже ошибаться в именах при вызове инструмента чтения.
В следующем уроке добавим на сервер третий примитив — промпты, а потом научим клиент показывать их как команды.
