Cloudflare держит сканер на isitagentready.com, который оценивает домен по готовности к ИИ-агентам. Двадцать одна проверка в пяти категориях: обнаружимость, доступность контента, контроль доступа ботов, discovery протоколов и коммерция.

Мы навели его на spintax.net — документационный сайт, неделей раньше выкативший полную поверхность llms.txt. Курируемый индекс, восемнадцать чистых Markdown-зеркал страниц документации и склеенный llms-full.txt на один запрос. Результат вернулся: 21 балл из 100, Level 1, «Basic Web Presence». Доступность контента — ноль из одного.

Вот этот разрыв и интересен. Оценка измеряет не то, насколько ваш сайт читаем для модели. Она измеряет, какие механизмы HTTP вы реализовали. Это разные вещи, и сайт, публикующий llms.txt, думает, что покупает первую.

Дальше — что аудит на самом деле измеряет, какие четыре проверки стоило внедрить и почему восемь провалов были правильными.

Почему полный llms.txt даёт ноль за доступность контента

Проверка ищет не файл. Она отправляет запрос с Accept: text/markdown и читает Content-Type, который приходит в ответ. Если ответ text/html, проверка провалена, сколько бы Markdown вы ни публиковали по другим адресам.

Это HTTP content negotiation, и он описан в спецификации с тех времён, когда агентов ещё никто не строил. Один адрес, несколько представлений, клиент заявляет предпочтение заголовком. llms.txt — это соглашение: файл по известному пути, о котором клиент должен знать заранее. Согласование форматов — это механизм: любой клиент, уже говорящий по HTTP, получит Markdown, просто попросив его, и даже не зная, что ваш сайт существует.

На диске у нас лежали восемнадцать Markdown-файлов, и клиент не мог до них добраться иначе, как прочитав сначала индекс. Аудит был прав.

Согласование форматов на Pages, на бесплатном тарифе

Cloudflare продаёт готовый вариант. Настройка уровня зоны под названием Markdown for Agents перехватывает ответы, когда запрос несёт Accept: text/markdown, и на лету конвертирует HTML в Markdown. Это один переключатель в AI Crawl Control или один PATCH к настройке зоны content_converter.

Доступна она на «Pro, Business and Enterprise plans, and SSL for SaaS customers at no cost». Сайт на Free.

К тому же переключатель был бы вариантом хуже, даже будь он доступен. Наши зеркала генерируются на сборке из того же HTML через конфигурацию turndown с собственными правилами для карточных сеток, которые иначе превращаются в нечитаемые блоки, вложенные в ссылки. Автоматическая конвертация на краю сети про них знать не может. Выточенный вручную вывод бьёт универсальный, когда вы его и так собираете.

Значит, Pages Function. Заметьте: _headers и _redirects эту задачу не решают вообще, потому что ни тот, ни другой не умеют ветвиться по заголовку запроса. Согласованию форматов нужен код.

// functions/_middleware.ts
export const onRequest = async (context) => {
  const { request, env, next } = context;
  const url = new URL(request.url);

  if (request.method === 'GET' && wantsMarkdown(request.headers.get('Accept'))) {
    const mirror = await env.ASSETS.fetch(
      new Request(new URL(mirrorFor(url.pathname), url.origin)),
    );
    if (mirror.ok) {
      return new Response(mirror.body, {
        status: 200,
        headers: {
          'Content-Type': 'text/markdown; charset=utf-8',
          'Vary': 'Accept',
          'X-Robots-Tag': 'noindex',
          'Link': `<${url.origin}${url.pathname}>; rel="canonical"`,
        },
      });
    }
  }

  const response = await next();
  const out = new Response(response.body, response);
  out.headers.append('Vary', 'Accept');
  return out;
};

Три детали здесь заслуживают своего места.

env.ASSETS.fetch читает уже задеплоенный статический файл. Функция ничего не конвертирует и никакой копии контента не держит.

Vary: Accept стоит на обеих ветках. У одного адреса теперь два представления, и кеш, который не учитывает этот заголовок, отдаст Markdown браузеру.

Запасной путь — это промах по зеркалу, а не ошибка. Если env.ASSETS.fetch вернёт 404, код уходит на HTML-ветку. Страница, у которой зеркала ещё нет, деградирует тихо — просто отдаёт HTML.

Определение предпочтения заслуживает большего, чем поиск подстроки. Браузеры никогда не шлют text/markdown, так что самого присутствия достаточно как сигнала, но клиент может написать Accept: text/markdown;q=0, и это значит ровно обратное:

function wantsMarkdown(accept) {
  if (!accept) return false;
  for (const entry of accept.split(',')) {
    const [type, ...params] = entry.split(';');
    if (type.trim().toLowerCase() !== 'text/markdown') continue;
    const q = params.map(p => p.trim()).find(p => p.toLowerCase().startsWith('q='));
    return !q || Number.parseFloat(q.slice(2)) > 0;
  }
  return false;
}

Результат проверяется из любого терминала, и в этом весь смысл механизма вместо соглашения:

$ curl -sI -H 'Accept: text/markdown' https://spintax.net/
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Link: <https://spintax.net/>; rel="canonical"
Vary: Accept
X-Robots-Tag: noindex

Как оставить статический сайт статическим

Корневой _middleware по умолчанию срабатывает на каждом запросе. На сайте из 116 страниц плюс ассеты это превращает полностью статический деплой в такой, где каждый запрос дёргает воркер — ради Markdown на девятнадцати из них.

Чинит это _routes.json в выводе сборки:

{
  "version": 1,
  "include": ["/", "/docs/", "/docs/syntax", "/docs/variables/"],
  "exclude": []
}

Мы генерируем этот файл из того же списка, из которого генерируются зеркала, поэтому маршрутизируемые адреса и адреса, участвующие в согласовании, разъехаться не могут. Девяносто семь локализованных страниц, 404 и все ассеты отдаются из статического хранилища и до функции не доходят.

Две ловушки Pages, в которые мы попали по дороге

Пересекающиеся правила _headers конкатенируются

Эта обошлась нам дороже всего по времени. Сайт уже отдавал свои Markdown-зеркала через вайлдкард:

/*.md
  Content-Type: text/plain; charset=utf-8
  X-Robots-Tag: noindex

Файлы навыков под /.well-known/agent-skills/ потребовали более специфичного правила — с text/markdown. Оба правила совпадают с /.well-known/agent-skills/spintax-syntax/SKILL.md. Результат:

Content-Type: text/plain; charset=utf-8, text/markdown; charset=utf-8

Pages слил два правила, приписав значения одно к другому. Специфичное правило не победило. На выходе — невалидный Content-Type, и ничто вас об этом не предупреждает.

Починили удалением вайлдкарда и генерацией одного явного правила на зеркало прямо в сборке. Восемнадцать правил вместо одного шаблона, что заметно ниже лимита в 100. Если два правила _headers могут совпасть с одним путём и задать заголовок с одним именем, считайте, что применятся оба.

wrangler pages dev разбирает _headers один раз

Локальный дев-сервер читает _headers при старте и пишет в лог, сколько правил разобрал. Перечитывать после пересборки он не будет. Мы починили вайлдкард, пересобрали, перепроверили — и увидели тот же сломанный Content-Type, потому что сервер всё ещё держал правила, прочитанные две сборки назад.

Перезапускайте дев-сервер после любого изменения _headers. Число правил в стартовом логе — самое быстрое подтверждение, что новый файл подхвачен.

Три дешёвые проверки

Content Signals. Директива в robots.txt, которая заявляет, как контент может использоваться ИИ-системами. Она идёт внутрь группы User-agent, под преамбулой с contentsignals.org, которая и несёт юридический вес:

User-agent: *
Content-Signal: ai-train=yes, search=yes, ai-input=yes

Allow: /

Три сигнала, у каждого три значения: search для индексации, ai-input для извлечения и обоснования ответов, ai-train для обучения моделей. У документационного сайта, публикующего Markdown-зеркала для машин, ответ на все три очевиден. Преамбулу оставьте. Именно она превращает строку из пожелания в оговорку о правах по статье 4 директивы ЕС об авторском праве.

Заголовки Link в ответе. RFC 8288, причём аудит принимает только зарегистрированные типы отношений. describedby для машиночитаемых описаний, alternate для другого представления той же страницы, service-doc для точки входа в документацию:

Link: </llms.txt>; rel="describedby"; type="text/plain",
      </docs/variables.md>; rel="alternate"; type="text/markdown",
      </docs/>; rel="service-doc"; type="text/html"

Агент, попавший на любую страницу, найдёт остальное прямо в заголовках ответа, не разбирая HTML и ничего не зная про llms.txt. Эта проверка ничего не стоит в рантайме: это статический вывод _headers, функция не нужна.

Agent Skills. Индекс discovery по адресу /.well-known/agent-skills/index.json, перечисляющий документы навыков, которые агент может установить. Единственная из четырёх, у которой есть ценность для продукта, а не только для аудита. Навык — это сжатый документ в повелительном наклонении, адресованный машине, которая вот-вот что-то сделает, и это другой артефакт, нежели страница документации, написанная для человека.

Записи индекса несут SHA256-дайджест каждого артефакта. Индекс надо генерировать, а не писать руками. Дайджест, набранный руками, станет неверным в тот же миг, когда кто-нибудь отредактирует навык, а неверный дайджест сообщает клиенту, что файл подменили. Мы читаем name и description из фронтматтера самого навыка и хешируем байты в том виде, в каком они отдаются, так что у метаданных один источник и второго списка вести не надо.

Восемь провалов, которые были правильными

Проверки, которые всё ещё провалены, — это DNS for AI Discovery, API Catalog, OAuth discovery, OAuth Protected Resource, Auth.md, MCP Server Card, A2A Agent Card и WebMCP. Рядом с ними стоят пять коммерческих проверок, и аудит помечает их нейтральными, а не проваленными, потому что видит, что сайт ничего не продаёт.

У документационного сайта нет ни API, ни аутентификации, и продавать ему нечего. Публикация пустого документа OAuth discovery ради оценки рекламирует сервер авторизации, которого не существует. Каждый такой документ — обещание клиенту, что он там что-то найдёт. Обещание, которое вы не можете сдержать, хуже отсутствующего файла: клиент, который его прочитал, тратит запрос впустую, а потом гадает, вы сломались или врёте.

Три из них заслуживают подробностей, потому что это рассуждение работает и в других случаях.

MCP Server Card указывает на путь, который спецификация бросила

Аудит проверяет три места для MCP Server Card: /.well-known/mcp.json, /.well-known/mcp/server-cards.json и /.well-known/mcp/server-card.json.

Предложение, на котором это держится, — SEP-2127: открыто и не смержено, статус Draft, Extensions Track. Нормативный формат передачи вообще не живёт в репозитории спецификации. Он живёт в репозитории с названием experimental-ext-server-card. Путь discovery переезжал как минимум трижды, и сейчас канонический — /.well-known/ai-catalog.json, с медиатипом application/ai-catalog+json, и это каталог, индексирующий карточки, а не карточка.

Ни один из трёх адресов, которые проверяет аудит, спецификация сейчас не использует. Пройти эту проверку сегодня — значит опубликовать по адресу, который спецификация уже покинула.

DNS-AID: проверка, которую не проходит её собственный автор

Этот провал стоит разобрать подробнее всех: он чище всего показывает, зачем провалы читают, а не закрывают.

DNS for AI Discovery просит опубликовать записи SVCB в пространстве имён _agents, чтобы агенты могли найти ваши агентские эндпоинты через DNS. Это чистая DNS-запись: ни воркера, ни рантайма, ни платы, а SVCB Cloudflare поддерживает на бесплатном тарифе. По обычной арифметике это самая дешёвая проверка во всём списке.

Три вещи говорят об обратном.

Спецификация — индивидуальный Internet-Draft. Версия 02, обновлена 27 мая 2026 года, не принята ни одной рабочей группой, без формального статуса в процессе стандартизации, и истекает 28 ноября 2026 года. Это не повод её игнорировать. Это повод понимать, на чём вы строите.

Сканер опрашивает метки, которых драфт не определяет. Он запрашивает _index._agents, _mcp._agents и _a2a._agents. Драфт определяет точку входа на _index._agents и выбирает протоколы через сервисный параметр alpn, а не через отдельную метку с подчёркиванием на каждый протокол. _mcp и _a2a придумал сам сканер, а не спецификация — то же расхождение, что и с MCP Server Card выше, и по той же причине.

Никто их не публикует. Не риторическое «никто». Проверено через DNS-over-HTTPS 22 июля 2026 года:

Домен _index._agents
isitagentready.com NXDOMAIN
cloudflare.com записей нет
agents.cloudflare.com записей нет

Сам сайт аудита и компания, которая этот аудит написала.

И вывод, который отсюда следует: наведите сканер на него самого. isitagentready.com показывает Level 4, Agent-Integrated, и dnsAid — среди его собственных провалов. Тот же уровень, до которого дошёл этот блог, — у инструмента, который не проходит собственную проверку.

Справедливости ради, одно возражение против публикации не работает. Соблазнительно сказать, что индексировать нечего — агентов у нас нет, — но драфт выносит содержимое индексного эндпоинта за скобки: это может быть и живой сервис, и статический документ, а сервисный параметр well-known указывает на метаданные по RFC 8615. Мы могли бы честно направить _index._agents на индекс Agent Skills, который уже публикуем.

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

A2A Agent Card называет живой эндпоинт

A2A Agent Card требует url для сервиса A2A. Она описывает агента, которому другие агенты могут передать работу. Мы такого не держим. Публикация карточки рекламировала бы сервис, который ни на что не отвечает.

Отсюда же и потолок оценки. Level 5 требует Auth.md, MCP Server Card, A2A Agent Card и API Catalog. Контентный сайт может честно дотянуться максимум до одного из них. Level 4 — верх диапазона для сайта, который только публикует документы, и это свойство лестницы, а не изъян сайта.

MCP-сервер, который мы не стали делать, и число, которое всё решило

Единственный пункт в списке с настоящей ценностью для нас — это MCP-сервер: дать агенту инструмент validate, возвращающий структурированную диагностику со строкой и колонкой, и петля от «модель пишет шаблон» до «модель чинит шаблон» замыкается без человека внутри.

Две находки отодвинули его с этой недели.

Протокол — посреди слома. Ревизия 2026-07-28 лишает Streamable HTTP состояния. Заголовок Mcp-Session-Id «and the protocol-level session that came with it are also removed», рукопожатие initialize/initialized убрано, а транспорт «now requires Mcp-Method and Mcp-Name headers so load balancers, gateways, and rate-limiters can route on the operation», с новым методом server/discover для capabilities. Сами авторы называют это крупнейшей ревизией протокола с момента запуска. Строить под текущую форму — значит переписывать через неделю.

Вторая находка — это число. Документация Cloudflare ясно говорит, что MCP-серверу без состояния не нужны ни Durable Objects, ни платный тариф, — и звучит это так, будто бесплатного тарифа хватит. А потом вы смотрите, что бесплатный тариф даёт: 10 мс CPU на запрос. Мы замерили движок, который собирались оборачивать, на Node, так что относитесь к этому как к порядку величины, а не к замеру целевой среды:

Работа CPU
Шаблон 400 байт, один рендер, прогретый от 0,13 до 0,28 мс
Шаблон 400 байт, двадцать вариантов от 3 до 6 мс
Первый вызов на холодном изоляте около 12,5 мс
Шаблон 16 КБ, двадцать вариантов около 85 мс

Один только холодный старт изолята уже выходит за бюджет — до того, как разобран хоть один шаблон. Шаблон в 16 КБ превышает бюджет в восемь раз. Надёжному серверу на бесплатном тарифе понадобились бы ограничения, достаточно тесные, чтобы раздражать: около 4 КБ шаблона и пять вариантов. Тариф Workers Paid за пять долларов в месяц поднимает предел до 30 секунд CPU и снимает вопрос.

Это проектное решение, которое принимают до написания кода, а не после.

Куда пришла оценка и где стоит этот сайт

Три пройденные проверки стали семью, а уровень поднялся с 1 до 4, «Agent-Integrated». Ещё шесть помечены нейтральными, потому что неприменимы, а восемь проваленных — ровно те, что и должны быть проваленными.

Полезным выводом было не число. Полезным было узнать, что неделя работы над llms.txt дала файлы, которые ни один клиент не смог бы запросить через согласование форматов, — а это конкретная и исправимая вещь, которую никакое чтение собственной документации не вскрыло бы.

Для протокола: этот блог на момент написания статьи набирал Level 1, три из двадцати одной — ровно там же, откуда стартовал spintax.net. Прогон тех же четырёх проверок на нём в тот же день дал другой ответ, и разница стоит больше оценки.

Все четыре внедрены, и теперь скан показывает Level 4 — тот же, что у сайта, о котором эта статья. Content Signals, заголовки Link, индекс Agent Skills и Markdown-зеркало каждой статьи, проиндексированное в llms.txt. Интереснее всего четвёртое, потому что форма сайта меняет его цену.

Этот сайт не на Pages. Это воркер, отдающий статические ассеты, и до сегодняшнего дня скрипта у него не было вовсе — поэтому «requests to static assets are free and unlimited» покрывало каждый просмотр страницы. Согласованию форматов нужен код перед этими ассетами, и любая страница, которая этот код выполняет, становится обычным запросом воркера и списывается из 100 000 в сутки на бесплатном тарифе.

Поэтому код выполняется на как можно меньшем числе адресов: в run_worker_first перечислены апекс и односегментные пути, и больше ничего. Стили, картинки, сами зеркала, robots.txt и sitemap до него не доходят и остаются бесплатными.

Одну деталь стоит украсть. Ключ кеша Cloudflare не включает Vary, если это не прописано Cache Rule, поэтому один адрес с двумя представлениями может отдать Markdown человеку из кеша. Чинится это не зональным правилом: это Cache-Control: no-store только на markdown-ветке. Агенты — тонкий ручеёк, HTML сохраняет своё обычное кеширование, и сценарий отказа исчезает.

Вторая деталь стыдная и дешёвая. Обрабатывайте HEAD, а не только GET. RFC 9110 говорит, что HEAD обязан отвечать теми же заголовками, что отдал бы GET, а первая версия этого не делала — о чём curl -I сообщил немедленно, потому что именно так любой и стал бы проверять это руками.

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

Прогоните его на собственном домене. А потом прочитайте провалы и решите, какие из них описывают сайт, который вы действительно держите:

curl -sS -X POST https://isitagentready.com/api/scan \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com"}'

JSON-эндпоинт возвращает каждую проверку со статусом, сообщением и списком сделанных запросов, это быстрее браузера, а результат удобнее сравнивать после деплоя. Он сообщает уровень, а не оценку; число из 100 — это уже веб-интерфейс.