Глава 12

Используйте resources для адресуемого контекста

Resource читается по URI и представляет данные, а не действие. Это принципиально иная поверхность, чем инструмент: ресурс отвечает на вопрос "дай содержимое объекта по этому адресу", а не "выполни операцию". Статические ресурсы подходят для известных документов, шаблоны - для параметризованных объектов. И то, и другое яснее, чем универсальный инструмент вида get_everything.

Статический ресурс - это фиксированный URI и содержимое, как наша политика выпуска.

TypeScript
  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.

TypeScript
  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 - шаблоны взаимодействия, и у неё своя тонкая граница власти.

Ссылки