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

Определяем ресурсы

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

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

Прямой ресурс

Добавьте в docs_server.py:

python
@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, имя — из имени функции. Здесь они предназначены не модели, а человеку и хосту: именно их покажет список ресурсов в интерфейсе.

Шаблон ресурса

Список — это хорошо, но нужен доступ к каждому документу. Заводить по ресурсу на файл вручную нельзя: файлов может быть сколько угодно. Для этого есть шаблоны:

python
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/listdocs://{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:

python
        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 мин на рабочем месте
  1. Добавьте ресурс docs://stats с типом application/json, который возвращает словарь: число документов и общий объём в символах. Прочитайте его в check.py и разберите JSON через json.loads.
  2. Если вы перевели DOCS на настоящую папку, замените схему на file:// и возвращайте абсолютный путь в имени ресурса. Проверьте, что инспектор показывает список.
  3. Подумайте, какие ресурсы стоило бы отдать серверу вашего трекера задач: список проектов? Схему полей задачи? Текущий спринт? Для каждого решите — прямой ресурс или шаблон.

Коротко

  • @mcp.resource("схема://путь") регистрирует прямой ресурс; {параметр} в URI делает его шаблоном.
  • Имя параметра в URI должно совпадать с аргументом функции; проверяется при импорте.
  • mime_type говорит клиенту, что за данные; списки и словари уходят как JSON.
  • Ошибки чтения — ResourceNotFoundError, а не is_error: ресурс читает приложение, исправлять некому.
  • Ресурс — для данных, которые прикладывают к вопросу заранее; инструмент — для действий, параметры которых подбирает модель.
  • Перечисление ресурсов бесплатно: функции выполняются только при чтении.

Видеоверсия

Сценарий озвучки · 495 слов, ≈ 4 мин

У сервера есть инструменты, и модель умеет читать документы сама. Но мы говорили, что есть второй путь: человек заранее прикладывает документ к вопросу, без решения модели и без подтверждения. Это ресурсы. Сегодня добавим их на сервер.

Первый ресурс — список документов. Декоратор «ресурс» принимает адрес: схема «докс», две косые черты, слово «лист». Схема наша собственная: протокол их не ограничивает, и для локальных данных принято придумывать говорящие. Функция возвращает список, SDK превращает его в джейсон, и мы указываем тип содержимого «application json», чтобы клиент знал: это данные, а не просто строка. Описание берётся из докстринга, но предназначено уже не модели, а человеку и хосту — именно его покажет список ресурсов в интерфейсе.

Второй ресурс — содержимое документа по имени. Заводить по ресурсу на файл вручную нельзя, файлов может быть сколько угодно. Для этого есть шаблоны: в адресе в фигурных скобках пишем имя параметра, и оно должно точно совпадать с аргументом функции. Когда клиент запросит документ «риски», SDK сопоставит адрес с шаблоном, вытащит имя и вызовет функцию. В протоколе прямые ресурсы и шаблоны живут в разных списках, а читаются одним методом.

Об ошибках. У ресурсов нет флага ошибки, как у инструментов: чтение либо удаётся, либо нет. Если документа нет, поднимаем специальное исключение «ресурс не найден», и клиент получит ошибку протокола. Это осмысленно: ресурс читает приложение, а не модель, исправлять некому — хост просто покажет ошибку пользователю.

Теперь главный вопрос: зачем два пути к одному документу — инструмент и ресурс? Это разные сценарии. Инструмент вызывает модель, когда по ходу задачи понимает, что ей нужен документ. «Проверь, согласуется ли оценка с брифом» — модель сама прочитает оба. Каждый вызов — её решение, подтверждение хоста, отдельный шаг. Ресурс подключает человек до того, как модель что-то решила. «Вот бриф, вот риски — что упущено?» — пользователь прикладывает документы, и модель получает их в первом же сообщении. Ни решений, ни подтверждений.

Практическое правило: если данные пассивные и их часто прикладывают целиком — это ресурс. Если получение данных — действие с параметрами, которые модель должна подобрать, — это инструмент. Списки, справочники, схемы — ресурсы. Поиск, фильтрация, запросы к API — инструменты. И ещё довод: перечисление ресурсов ничего не стоит, функции выполняются только при чтении.

Проверяем из клиента: печатаем список ресурсов и шаблонов, читаем список документов, читаем один документ, пробуем несуществующий и ловим исключение. Потом открываем инспектор: во вкладке ресурсов появились и прямой ресурс, и шаблон с полем для параметра.

Небольшое предупреждение о конфликте адресов. У нас есть прямой ресурс «список» и шаблон «документ по имени», и формально слово «список» подходит под шаблон. SDK сначала проверяет точные совпадения, поэтому прямой ресурс побеждает. Но в настоящих серверах таких пересечений лучше не создавать: назовите список иначе, если документ с таким именем возможен.

Задание для самостоятельной работы: добавьте ресурс со статистикой — число документов и общий объём — в формате джейсон, прочитайте его из клиента и разберите. А потом подумайте, какие ресурсы стоило бы отдать серверу вашего трекера задач: список проектов, схему полей, текущий спринт. Для каждого решите, прямой это ресурс или шаблон.

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

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