Урок курса
Концепция 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.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 7
Сводка по resources/list и resources/read
Реализуйте функцию, которая имитирует работу клиента MCP с ресурсами сервера.
На вход в payload уже переданы материализованные данные о зарегистрированных ресурсах и запросах клиента.
Нужно вернуть структурированный результат, который содержит:
- список доступных ресурсов для
resources/list; - ответ на
resources/readдля одного запрошенного URI; - флаг корректности проверки.
Правила:
- в
payloadесть поля: resources— список словарей с описанием ресурсов;read_uri— URI, который клиент хочет прочитать.- каждый ресурс в
resourcesимеет поля: urinamedescriptionmimeTypetextresources/listдолжен содержать только метаданные ресурса:urinamedescriptionmimeTyperesources/readдолжен вернуть содержимое только для ресурса с URI изread_uri.- если ресурс найден, ответ должен иметь вид:
ok:Truelist_result: словарь с ключомresourcesread_result: словарь с ключомcontents, гдеcontents— список из одного объекта сuri,mimeType,text- если ресурс не найден, вернуть:
ok:Falseerror: короткое сообщение
Дополнительно:
- порядок ресурсов в
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 и понятную ошибку.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
