Используйте resources для адресуемого контекста
Resource читается по URI и представляет данные, а не действие. Это принципиально иная поверхность, чем инструмент: ресурс отвечает на вопрос "дай содержимое объекта по этому адресу", а не "выполни операцию". Статические ресурсы подходят для известных документов, шаблоны - для параметризованных объектов. И то, и другое яснее, чем универсальный инструмент вида get_everything.
Статический ресурс - это фиксированный URI и содержимое, как наша политика выпуска.
server.registerResource(
"release-policy",
"runbook://release-policy",
{
title: "Политика выпуска",
description: "Критерии допуска релиза",
mimeType: "text/markdown",
cacheHint: { ttlMs: 60_000, cacheScope: "public" }
},
async (uri) => {
const runbook = getRunbook("release-policy");
if (!runbook) {
throw new Error("Runbook release-policy is missing");
}
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: runbook.body }] };
}
);Шаблон описывает целое семейство адресов - например, снимок любого релиза по service и version.
server.registerResource(
"release-snapshot",
new ResourceTemplate("release://{service}/{version}", {
list: async () => ({
resources: listReleaseSnapshots().map((snapshot) => ({
uri: `release://${snapshot.service}/${snapshot.version}`,
name: `${snapshot.service}@${snapshot.version}`,
title: `Снимок релиза ${snapshot.service}@${snapshot.version}`,
mimeType: "application/json"
}))
})
}),
{
title: "Снимок релиза",
description: "Факты CI, тестов, безопасности и отката",
mimeType: "application/json",
cacheHint: { ttlMs: 10_000, cacheScope: "private" }
},
async (uri, variables) => {
const service = singleVariable(variables.service, "service");
const version = singleVariable(variables.version, "version");
const snapshot = getReleaseSnapshot(service, version);
if (!snapshot) {
throw new Error(`Release ${service}@${version} not found`);
}
return {
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(snapshot, null, 2) }]
};
}
);Когда что выбирать, удобно решать по нескольким осям сразу.
Вопрос · Resource · Tool
- Семантика - "Дай содержимое объекта по URI"; "Выполни операцию с args"
- Discovery - List + URI templates; Tool list
- Кэш - Естественный по URI и version; Зависит от операции
- Side effect - Не ожидается; Возможен и должен быть объявлен
- Пример -
release://web-portal/2.4.0;review_release
Смысл разделения в семантике доступа. У ресурса естественный кэш - по URI и версии; побочный эффект не ожидается; discovery идёт через список и URI-шаблоны. У инструмента кэш зависит от операции, побочный эффект возможен и должен быть объявлен. Путать их - значит терять и предсказуемость кэша, и ясность намерения.
URI при этом сам является контрактом. Выбирайте стабильную схему, нормализуйте переменные и не позволяйте шаблону обойти tenant boundary. В примере сервер принимает только одну непустую строку на каждую переменную, а поиск выполняет доменный repository - не сам протокольный слой.
Resource content тоже недоверен. Документ может содержать prompt injection. Host должен помечать происхождение, ограничивать объём и не превращать текст ресурса в системные инструкции - иначе адресуемый контекст становится каналом атаки.
Инструменты и ресурсы закрывают действия и данные. Осталась третья поверхность MCP - шаблоны взаимодействия, и у неё своя тонкая граница власти.