AmigaОбучение ИИ
Модуль 5 · Подключаем клиент · урок 11 из 14

Доступ к ресурсам

10 мин чтения▶ есть видеоверсия

На сервере ресурсы есть, теперь нужно научить клиент ими пользоваться. Напомним, что ресурсы контролирует приложение: модель их не вызывает, она получает содержимое уже в запросе. Значит, клиент должен решить, какие ресурсы приложить, прочитать их и вставить в сообщение. В этом уроке сделаем это через упоминания @имя — так же, как работает @сервер:ресурс в Claude Code.

Перечислить и прочитать

Два метода клиента, которые нам нужны, вы уже видели в check.py:

python
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:

python
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:

python
            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 покажет её пользователю. Модель до этого не дойдёт — и правильно: ошибку в имени файла должен исправлять человек, а не модель.

Запуск

bash
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 мин на рабочем месте
  1. Выведите список ресурсов и шаблонов при старте клиента, рядом со списком инструментов. Используйте uri_template для шаблонов, чтобы пользователь видел синтаксис.
  2. Сделайте так, чтобы docs://list прикладывался к каждому запросу автоматически, в отдельном теге <available-documents>. Проверьте, что модель стала реже ошибаться в именах при вызове read_doc.
  3. Замените для 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 делает ровно то же самое, только с именем сервера перед адресом. Находит клиент нужного сервера, читает ресурс, вставляет содержимое. И ещё показывает список ресурсов при наборе собачки — для этого и нужно перечисление: оно бесплатно, а пользователь видит, что можно приложить.

Мы вставили ресурс целиком, но протокол этого не требует. Приложение решает само: приложить целиком, найти и приложить только нужный фрагмент, приложить автоматически без упоминания — например, всегда добавлять список документов, чтобы модель знала, что существует. Тип содержимого подсказывает способ: маркдаун вставить как есть, джейсон разобрать, картинку отправить как картинку.

У нас список ресурсов пока не используется. Хорошее упражнение — вывести его при старте клиента рядом со списком инструментов, чтобы пользователь видел, что можно приложить. Второе упражнение — прикладывать список документов к каждому запросу автоматически, в отдельном теге. Тогда модель будет знать, какие документы существуют, и реже ошибаться в именах при вызове инструмента чтения.

В следующем уроке добавим на сервер третий примитив — промпты, а потом научим клиент показывать их как команды.

Отметка хранится только в вашем браузере