Урок курса

Концепция MCP resource: URI, метаданные и типы контента

AI Agent Integrations PRO: MCP

Предыдущие уроки разбирали, как описывать инструменты (tools) и делать их вызов надёжным через примеры в контракте. Теперь смещаемся к другому примитиву MCP — ресурсам. Если tool — это действие, которое сервер выполняет по запросу, то resource — это данные, которые сервер делает доступными для чтения.

MCP Resource: что это и из чего состоит

Resource в MCP — это именованная единица данных, которую сервер публикует для клиента. Клиент может узнать, какие ресурсы существуют, и запросить содержимое любого из них по адресу. Никаких вычислений, никаких побочных эффектов — только чтение. Каждый ресурс описывается четырьмя полями, и все они несут конкретную нагрузку. URI — уникальный адрес ресурса внутри сервера. Это произвольная строка, но она должна быть стабильной: клиент запоминает URI и обращается к нему повторно. MCP не навязывает конкретную схему, но на практике используют file://, resource:// или кастомные схемы вроде db:// или config://. Главное требование — предсказуемость: клиент не должен угадывать адрес. mimeType — тип содержимого. Это поле говорит клиенту, что именно он получит: text/plain, application/json, text/markdown, image/png. Без явного MIME-типа клиент вынужден угадывать формат по содержимому, что ненадёжно — особенно когда данные передаются AI-агенту, который использует их в следующем шаге обработки. name — короткое человекочитаемое название ресурса. Используется для отображения в интерфейсе или при выборе ресурса агентом из списка. description — развёрнутое описание того, что содержит ресурс. Именно по description AI-агент решает, нужен ли ему этот ресурс, без загрузки его содержимого. Хорошее description снижает лишние запросы и помогает агенту выбрать правильный источник данных. Вместе эти четыре поля образуют «паспорт» ресурса: URI говорит где, mimeType говорит в каком формате, name и description говорят что и зачем. Когда все четыре заполнены осознанно, клиент получает достаточно информации для работы ещё до того, как запросит само содержимое.

Регистрация ресурса: процедура и пример

Когда концептуальная модель ресурса понятна, остаётся перевести её в код. В MCP SDK (TypeScript) для этого используется server.addResource() — метод, который связывает описание ресурса с handler-ом, возвращающим содержимое. Вот полный пример регистрации текстового ресурса с JSON-конфигом:


import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "config-server",
  version: "1.0.0",
});

server.addResource(
  {
    uri: "resource://config/app",
    name: "App Config",
    description: "Текущий конфиг приложения: версия, режим отладки",
    mimeType: "application/json",
  },
  async () => ({
    contents: [
      {
        uri: "resource://config/app",
        mimeType: "application/json",
        text: JSON.stringify({ version: "1.0", debug: false }),
      },
    ],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

addResource() принимает два аргумента. Первый — объект-описание ресурса: те самые uri, name, description, mimeType. Второй — async-функция, которую сервер вызывает при запросе содержимого. Она должна вернуть объект с полем contents — массивом объектов, каждый из которых содержит uri, mimeType и либо text (строка), либо blob (base64). Обратите внимание: uri указывается дважды — в описании ресурса и внутри contents[]. Это не случайность: протокол требует явного совпадения, чтобы клиент мог однозначно соотнести запрошенный адрес с возвращённым содержимым. Расхождение между этими двумя URI — одна из самых частых причин, по которым клиент игнорирует ответ сервера. Если у сервера несколько ресурсов, каждый регистрируется отдельным вызовом addResource(). Handler-ы изолированы: один ресурс может читать файл с диска, другой — делать запрос к базе данных, третий — собирать данные из переменных окружения. Протоколу всё равно, откуда берётся содержимое, — важен только формат ответа. После запуска сервера клиент сможет обнаружить ресурс через resources/list и получить его содержимое через resources/read — но об этом в следующей секции.

Обнаружение и чтение ресурсов: resources/list и resources/read

После того как ресурс зарегистрирован на сервере, клиент взаимодействует с ним через два RPC-метода: resources/list и resources/read. Это стандартная двухшаговая модель: сначала узнать, что есть, потом взять конкретное. resources/list — метод обнаружения. Клиент вызывает его без параметров, сервер возвращает массив объектов, каждый из которых описывает один ресурс:


{
  "resources": [
    {
      "uri": "resource://config/app",
      "name": "App Config",
      "description": "Текущий конфиг приложения: версия, режим отладки",
      "mimeType": "application/json"
    }
  ]
}

Клиент или AI-агент получает этот список и может выбрать нужный ресурс, не загружая его содержимого. Именно здесь description отрабатывает свою ценность: агент читает описание и решает, нужен ли ему этот источник данных для текущей задачи. Без resources/list клиенту пришлось бы знать URI заранее — то есть хардкодить адреса, что ломает гибкость всей схемы. resources/read — метод получения содержимого. Клиент передаёт URI конкретного ресурса:


{
  "uri": "resource://config/app"
}

Сервер находит зарегистрированный handler по этому URI, вызывает его и возвращает ответ:


{
  "contents": [
    {
      "uri": "resource://config/app",
      "mimeType": "application/json",
      "text": "{\"version\":\"1.0\",\"debug\":false}"
    }
  ]
}

Поле contents — это массив. В большинстве случаев в нём один элемент, но протокол допускает несколько: например, когда один URI агрегирует несколько связанных фрагментов. Содержимое ресурса передаётся либо как text (строка в любой кодировке), либо как blob — строка в base64 для бинарных данных вроде изображений или PDF. Важная граница между двумя методами: resources/list никогда не возвращает содержимое, только метаданные. resources/read не возвращает метаданные — только содержимое. Это разделение позволяет клиенту сначала дёшево просмотреть каталог, а потом точечно загрузить то, что нужно, не тратя трафик и контекстное окно на ненужные ресурсы.

Типичные ошибки при описании ресурса

Когда регистрация выглядит правильно, а клиент всё равно не получает содержимое — почти всегда виноваты два конкретных места: URI и MIME-тип. Разберём оба. Несоответствие URI в регистрации и ответе handler-а

Проблема в том, что URI указывается дважды — в описании ресурса (первый аргумент addResource) и внутри объекта contents[], который возвращает handler. Если они различаются, клиент не может однозначно связать запрошенный адрес с полученным ответом и либо выдаёт ошибку, либо молча игнорирует результат. Вот как выглядит сломанный вариант:


server.addResource(
  {
    uri: "resource://config/app",  // зарегистрированный URI
    name: "App Config",
    mimeType: "application/json",
  },
  async () => ({
    contents: [
      {
        uri: "resource://config/app-settings",  // ← другой URI, клиент теряется
        mimeType: "application/json",
        text: JSON.stringify({ version: "1.0" }),
      },
    ],
  })
);

Исправление простое: contents[].uri должен быть буквально той же строкой, что и URI в описании. Самый надёжный способ — вынести URI в константу и использовать её в обоих местах:


const RESOURCE_URI = "resource://config/app";

server.addResource(
  {
    uri: RESOURCE_URI,
    name: "App Config",
    mimeType: "application/json",
  },
  async () => ({
    contents: [
      {
        uri: RESOURCE_URI,  // та же константа — не может разойтись
        mimeType: "application/json",
        text: JSON.stringify({ version: "1.0" }),
      },
    ],
  })
);

Константа убирает целый класс опечаток и делает рефакторинг безопасным. Неверный MIME-тип

Вторая ошибка — объявить один MIME-тип, а вернуть данные в другом формате, либо вообще не указать mimeType. Типичный случай: handler возвращает JSON-строку, а в описании стоит text/plain или поле пропущено совсем.


server.addResource(
  {
    uri: "resource://config/app",
    name: "App Config",
    mimeType: "text/plain",  // ← неверно: данные — JSON, не plain text
  },
  async () => ({
    contents: [
      {
        uri: "resource://config/app",
        mimeType: "text/plain",  // ← та же ошибка продублирована в ответе
        text: JSON.stringify({ version: "1.0", debug: false }),
      },
    ],
  })
);

AI-агент, получив такой ответ, не будет автоматически парсить содержимое как JSON — он будет работать с ним как с произвольным текстом. Это ломает сценарии, где агент передаёт данные дальше по цепочке или использует их в структурированном виде. Правило простое: mimeType должен описывать реальный формат того, что лежит в text или blob. Для JSON — application/json, для Markdown — text/markdown, для PNG — image/png. Если отдаёте бинарные данные через blob, MIME-тип становится ещё важнее, потому что это единственная подсказка клиенту о том, как декодировать base64-строку. Оба pitfall объединяет одна механика: несоответствие между объявлением и реальностью. В случае URI — между адресом в описании и адресом в ответе. В случае MIME — между заявленным форматом и фактическим содержимым. Явная константа для URI и осознанный выбор MIME-типа на этапе проектирования ресурса закрывают обе проблемы до того, как они проявятся в рантайме.

Задача 1

Какое поле в описании MCP-ресурса сообщает клиенту формат содержимого (JSON, текст, изображение)?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Клиент хочет узнать, какие ресурсы предоставляет MCP-сервер, не зная их URI заранее. Какой метод он должен вызвать?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Разработчик зарегистрировал ресурс с uri: 'resource://logs/today', но в handler возвращает contents[{ uri: 'resource://logs/current', ... }]. Что произойдёт при вызове resources/read?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Ресурс возвращает строку вида {"version":"1.0","debug":false}. Какой MIME-тип следует указать в поле mimeType?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 5

Ты регистрируешь ресурс — файл README.md. Напиши корректный MIME-тип для Markdown-содержимого (общепринятое значение).

Без регистрации · результат не сохраняется

Задача 6

Регистрация MCP resource с проверкой URI и MIME-метаданных

Реализуйте функцию, которая подготавливает описание MCP resource и проверяет его согласованность с содержимым.

Нужно обработать один ресурс и вернуть структурированный результат для регистрации.

Требования:

  • взять из payload поля:
  • resource_uri — URI ресурса;
  • name — короткое название;
  • description — описание;
  • mime_type — MIME-тип;
  • content_uri — URI, который будет возвращён в contents;
  • content_text — текст содержимого ресурса.
  • проверить, что:
  • resource_uri не пустой;
  • resource_uri совпадает с content_uri;
  • mime_type не пустой;
  • если mime_type равен application/json, то content_text должен быть корректной JSON-строкой;
  • name и description не пустые.
  • если проверка проходит, вернуть словарь с ключами:
  • resource — описание ресурса;
  • contents — список из одного объекта с uri, mimeType, text;
  • is_valid — True.
  • если проверка не проходит, вернуть словарь:
  • is_valid — False;
  • error — короткое диагностическое сообщение.

Формат результата должен быть детерминированным и совпадать с контрактом тестов.

Example test

Входные данные содержат URI resource://config/app, MIME-тип application/json и JSON-текст {"version":"1.0","debug":false}. Результат должен содержать согласованные resource.uri и contents[0].uri, а также is_valid: True.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 7

Сводка по resources/list и resources/read

Реализуйте функцию, которая имитирует работу клиента MCP с ресурсами сервера.

На вход в payload уже переданы материализованные данные о зарегистрированных ресурсах и запросах клиента.

Нужно вернуть структурированный результат, который содержит:

  • список доступных ресурсов для resources/list;
  • ответ на resources/read для одного запрошенного URI;
  • флаг корректности проверки.

Правила:

  • в payload есть поля:
  • resources — список словарей с описанием ресурсов;
  • read_uri — URI, который клиент хочет прочитать.
  • каждый ресурс в resources имеет поля:
  • uri
  • name
  • description
  • mimeType
  • text
  • resources/list должен содержать только метаданные ресурса:
  • uri
  • name
  • description
  • mimeType
  • resources/read должен вернуть содержимое только для ресурса с URI из read_uri.
  • если ресурс найден, ответ должен иметь вид:
  • ok: True
  • list_result: словарь с ключом resources
  • read_result: словарь с ключом contents, где contents — список из одного объекта с uri, mimeType, text
  • если ресурс не найден, вернуть:
  • ok: False
  • error: короткое сообщение

Дополнительно:

  • порядок ресурсов в list_result['resources'] должен совпадать с порядком во входном списке;
  • в read_result['contents'][0]['uri'] должен быть ровно тот URI, который был запрошен;
  • read_result['contents'][0]['mimeType'] должен совпадать с MIME-типом найденного ресурса.

Example test

Если во входе есть ресурс resource://config/app с mimeType = application/json, то resources/list должен вернуть только его метаданные, а resources/read — один элемент contents с тем же URI и текстом ресурса.

Если запрошенный URI отсутствует, результат должен содержать ok: False и понятную ошибку.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку