MCP Server Card: путь, схема и проверка карточки
MCP Server Card — JSON-файл, по которому клиент находит ваш удалённый MCP-сервер. Поля, хостинг, проверка через curl и статус черновика на октябрь 2026.

MCP Server Card — карточка MCP-сервера — это небольшой публичный JSON-документ, который сообщает клиенту, где живёт ваш удалённый MCP-сервер и как к нему подключиться: имя, версия, URL эндпоинта, транспорт, поддерживаемые версии протокола — ещё до открытия соединения. Это метаданные для обнаружения, а не хэндшейк: она никого не аутентифицирует и не перечисляет ваши инструменты.
В стеке готовности сначала идут читаемые страницы, а поверх них — поверхности, доступные для вызова. Руководство о том, как сделать сайт доступным для вызова агентами, ставит карточки серверов рядом с Agent Skills и карточками A2A. Это руководство посвящено самой карточке: каждое поле, хостинг, проверка и то, на каком этапе стандартизации она находится в октябре 2026 года. Только знакомитесь с базовым слоем? Начните с материала о том, что такое готовность к ИИ-агентам.
Ключевые выводы
- Карточка отвечает на один вопрос до любого соединения: где сервер и какие версии протокола он поддерживает. Инструменты, capabilities и вход остаются в живых вызовах (SEP-2127).
- Официальным стандартом она пока не стала. SEP-2127 — открытый pull request с меткой
in-review, а текущая ревизия спецификации 2026-07-28 карточки не определяет (версионирование MCP).- Расположение менялось. Текущий черновик рекомендует
<mcp-url>/server-cardи AI Catalog по адресу/.well-known/ai-catalog.json; старые черновики использовали/.well-known/mcp/server-card.json, и сканеры, наш в том числе, его по-прежнему опрашивают.- Схема черновика требует
$schema,name,versionиdescription; чтобы карточка была полезной, добавьте записьremotesс абсолютнымurl(schema.json).- Проверка
mcp_server_cardв aiSiteReady читает карточки и в форме черновика, и в старой форме: по старым путям.well-known, по/server-cardи через AI Catalog. Но MCP-сессию она не открывает никогда, так что эндпоинт проверьте сами черезcurl.
Какую задачу решает MCP Server Card?
Она позволяет клиенту узнать об удалённом MCP-сервере, не зная заранее, куда подключаться. Сегодня человек вставляет URL эндпоинта в конфиг клиента. С карточкой клиент, IDE или краулер читает один статический документ, показывает имя, выбирает транспорт, сверяет версии протокола и лишь потом подключается. В SEP как сценарии названы автонастройка, обход доменов в поисках серверов и показ метаданных без обращения к каждому эндпоинту (SEP-2127).
Карточка намеренно маленькая. Таблица показывает, что она несёт, а что обязано оставаться «живым».
| Вопрос клиента | Есть в карточке? | Авторитетный ответ |
|---|---|---|
| Где сервер, по какому транспорту? | Да, remotes[] | Само соединение |
| Какие версии протокола? | Подсказка, supportedProtocolVersions | server/discover (2026-07-28) или initialize (раньше) |
| Имя, версия, описание? | Да, справочно | Идентификационные данные, которые сервер сообщает в рантайме |
| Какие инструменты, ресурсы, промпты? | Нет, по замыслу | tools/list, resources/list, prompts/list |
| Кто вправе вызывать, как войти? | Нет | 401 плюс WWW-Authenticate и метаданные RFC 9728 |
Инструменты исключены сознательно: статический документ не может описать поверхность, которая зависит от пользователя, сессии и feature flags (SEP-2127). Даже те поля, что в карточке есть, носят справочный характер. Клиентам нельзя опираться на них при решениях о доступе, а при расхождении стоит доверять живым значениям (discovery.md).
Хэндшейк тоже изменился. Ревизия 2026-07-28 убрала initialize и сессии на уровне протокола; каждый запрос несёт свою версию протокола, а server/discover — обязательный RPC, который клиент может вызвать первым (changelog). Серверы на ревизиях 2025-11-25 и более ранних по-прежнему начинают с initialize, а supportedProtocolVersions в карточке показывает, с какой эпохой клиент встретится.
Когда я делал карточку для aiSiteReady, самым дешёвым путём к зелёной проверке была статическая карточка, указывающая на любой корректный URL, ведь сканер проверяет форму карточки, а не хэндшейк. Я от этого отказался (почему — объясняет запись об архитектурном решении в проекте). MCP-эндпоинт обязан говорить на JSON-RPC, а наш REST API не говорит, так что карточка объявляла бы эндпоинт, которым не смог бы воспользоваться ни один MCP-клиент.
Вместо этого я написал настоящий MCP-сервер с одним инструментом scan_url, развернул его на /api/mcp и направил карточку туда. Пока я писал это руководство, выяснилось, что в продакшене он отклонял любой публичный запрос с 421, а карточка при этом проходила проверку. Сейчас это исправлено; история ниже.
Стала ли карточка MCP-сервера официальным стандартом?
Нет. На 2 октября 2026 года это предложение на рассмотрении: в ядро спецификации MCP оно не входит и в IANA не зарегистрировано.
- Предложение. SEP-2127 — pull request трека Extensions, открытый 21 января 2026 года как продолжение SEP-1649. Метку
in-reviewон носит с 7 августа и на 2 октября, когда я проверял, был ещё открыт (pull request). Файл SEP в его ветке уже помечен «Final», но ничего не влито. - Схема. Она живёт в репозитории, который переименовали из
experimental-ext-server-cardвext-server-card, но в README которого работа по-прежнему названа экспериментальной, а не принятым расширением MCP (репозиторий). - Ядро спецификации. Актуальна ревизия 2026-07-28 (версионирование). Карточки в ней нет, а в дорожной карте от 22 августа 2026 года серверные карточки не входят в пять приоритетов (дорожная карта).
- SDK. Два pull request — для Python и Go, — которые SEP называет референсными реализациями, оставались открытыми (python-sdk #2951, go-sdk #1024). Не считайте, что конкретный клиент читает карточки.
- IANA. В реестре well-known URI, обновлённом 16 сентября 2026 года, есть
agent-card.json,api-catalogиoauth-protected-resource, но нет ничего дляmcp,ai-catalogилиserver-card(IANA).
Где должна лежать карточка MCP-сервера?
Черновики расходятся и в том, где карточка должна лежать.
В самом SEP-1649 названы два пути: /.well-known/mcp.json в аннотации и /.well-known/mcp/server-card.json в тексте (SEP-1649). SEP-1960 хотел /.well-known/mcp и был закрыт на следующий день после открытия (SEP-1960).
Текущий черновик отказывается от .well-known для самой карточки: допустим любой незарезервированный URI, рекомендован <streamable-http-url>/server-card, а клиенты находят её через AI Catalog. Довод такой: .well-known предназначен для метаданных всего сайта, карточка описывает одно приложение, а URL и так сообщает каталог (discovery.md). Сам каталог — тоже черновик (ai-catalog.io).
Разошлись и те, кто публикует карточки. 2 октября 2026 года https://github.com/.well-known/ai-catalog.json содержал одну запись типа application/mcp-server-card+json со ссылкой на https://api.githubcopilot.com/mcp/server-card — эта карточка проходит валидацию по экспериментальной схеме, — тогда как nuxt.com по-прежнему отдаёт старую форму serverInfo плюс endpoints по адресу /.well-known/mcp/server-card.json. Это мои наблюдения через curl, а не обзор рынка.
Моя рекомендация — оценочное суждение, а не часть какого-либо черновика: публикуйте карточку там, где указывает черновик, с записью в каталоге, а тот же файл дублируйте на /.well-known/mcp/server-card.json. Дубль стоит один статический файл и сохраняет работоспособность сегодняшних сканеров и старых клиентов.
Какие поля содержит валидная MCP Server Card?
Валидной карточке нужны четыре поля — $schema, name, version и description, — а на практике ещё запись remotes с type и абсолютным url. Последний столбец показывает, что делает с каждым полем проверка aiSiteReady по состоянию на октябрь 2026 года.
| Поле | Обязательно | Что с ним делает клиент | Проверка aiSiteReady |
|---|---|---|---|
$schema | Да | Называет формат карточки (URL схемы v1) | Не требуется; вместе с remotes[] обозначает форму черновика |
name | Да | Стабильный id, reverse-DNS, один слэш: com.example/weather | Не должно быть пустым; подходят и шаблон черновика с одним слэшем, и имя в стиле reverse-DNS с точкой; старые формы: любое имя |
version | Да | Версия сервера, рекомендован semver | Не требуется |
description | Да, 1–100 символов | Строка для людей | Нужна для распознавания формы, если нет $schema; описание длиннее 100 символов всё равно проходит |
title, websiteUrl, icons, repository | Нет | Название, сайт, иконки, исходники | Игнорируются |
remotes[] | Нет, но нужен для подключения | Эндпоинты, к которым подключаются | Каждый url, достроенный относительно адреса карточки, должен быть корректным абсолютным URL; ноль remotes всё равно проходит |
remotes[].type | Да, внутри remote | streamable-http или sse | Записывается; неизвестные типы отмечаются, но не приводят к FAIL |
remotes[].url | Да, внутри remote | Абсолютный http(s) URL или шаблон с {variable} | Относительный достраивается и принимается; шаблон с {variable} принимается как есть; localhost, хост из одной метки или порт, кроме 80 и 443, дают PARTIAL |
remotes[].supportedProtocolVersions | Нет | Позволяет выбрать версию до подключения | Читается; значение не в формате даты YYYY-MM-DD даёт PARTIAL |
remotes[].headers, remotes[].variables, _meta | Нет | Заголовки, значения от пользователя, расширения вендора | Игнорируются |
Список взят из schema.json черновика. Одна странность: требуемый ею URL в $schema 2 октября отвечал 404, так что считайте его идентификатором и валидируйте по #/$defs/ServerCard внутри файла схемы. Корневой схемы в файле нет, поэтому валидатор, которому передан файл как есть, примет любой JSON.
Самая маленькая валидная карточка состоит из четырёх обязательных полей. Она проходит валидацию, но подключиться по ней нельзя: remote в ней не указан.
{
"$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
"name": "com.example/weather",
"version": "1.0.0",
"description": "Weather lookups and forecasts."
}
Полезная карточка добавляет remote и подсказку о версиях протокола, чтобы клиент мог решить ещё до подключения.
{
"$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
"name": "com.example/weather",
"version": "1.0.0",
"description": "Weather lookups and forecasts.",
"title": "Example Weather",
"websiteUrl": "https://example.com",
"remotes": [
{
"type": "streamable-http",
"url": "https://api.example.com/mcp",
"supportedProtocolVersions": ["2025-11-25", "2026-07-28"]
}
]
}
Оба документа проходят #/$defs/ServerCard из schema.json черновика в валидаторе JSON Schema 2020-12.
Там, где черновик и сканер расходятся, проверка мягче к содержимому и шире по путям. По состоянию на октябрь 2026 года она распознаёт пять форм карточки:
- текущий черновик, который проверка называет
server_card_v1; - список
serverInfoплюсendpoints[]из более раннего черновика, который отдаёт nuxt.com; - форму SEP-1649 с
serverInfoиtransport; - объект
mcp_versionплюсendpointsиз SEP-1960; - документ обнаружения в стиле платформ документации.
Формы serverInfo плюс endpoints[], SEP-1649 и SEP-1960 по-прежнему принимаются, но в доказательствах отчёта помечаются как устаревшие. Карточка в форме черновика без $schema или version или с описанием длиннее 100 символов тоже проходит. Каждое отступление от схемы черновика проверка записывает в доказательства и статус не меняет, потому что форма пока остаётся черновиком.
До 4 октября 2026 года моя собственная карточка была в более ранней форме serverInfo плюс endpoints[]. В проверке aiSiteReady она получала чистый pass, но экспериментальную схему не проходила по четырём причинам:
- у неё не было
$schemaиversion; - в имени не было слэша;
- описание занимало 269 символов;
- в ней был список
tools, который черновик исключает намеренно.
Такова была цена того, что я ориентировался на терпимость сканера, а не на черновик. Теперь карточка в текущей форме. Вот боевой документ целиком:
curl -sS https://www.ai-siteready.com/.well-known/mcp/server-card.json
{
"$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
"name": "com.ai-siteready/aisiteready",
"title": "aiSiteReady",
"description": "Scan a public URL for AI-agent readiness and get a scored, actionable report.",
"version": "0.1.0",
"websiteUrl": "https://www.ai-siteready.com/",
"remotes": [
{
"type": "streamable-http",
"url": "https://www.ai-siteready.com/api/mcp/",
"supportedProtocolVersions": [
"2025-06-18"
]
}
]
}
Она проходит #/$defs/ServerCard, а юнит-тест сверяет её с копией схемы в репозитории, так что изменение, которое нарушает соответствие, роняет CI. Проверка по-прежнему ставит ей pass: форма server_card_v1, отступлений нет.
Одну часть собственной рекомендации я пока не выполнил. По состоянию на октябрь 2026 года карточка лежит только по старому пути, без /api/mcp/server-card и без AI Catalog, так что клиент, который следует текущему черновику, её не найдёт.
Как разместить карточку и связать её с настоящим MCP-эндпоинтом?
Отдавайте её как статический публичный GET по HTTPS с JSON-типом содержимого, открытым CORS и валидаторами кэша, а настоящий эндпоинт указывайте в remotes[].url абсолютным URL. Черновик просит следующего (discovery.md).
- Тип содержимого.
application/mcp-server-card+jsonвезде, где размещена карточка; клиентам рекомендовано слать его вAccept. Проверка aiSiteReady принимаетapplication/jsonи любой тип+json, всё остальное помечает как PARTIAL. - CORS.
Access-Control-Allow-Origin: *, с разрешённымGETи открытымETag, чтобы карточку могли прочитать браузерные клиенты. Это безопасно, потому что карточка публична и доступна только для чтения. - Кэширование.
Cache-Control: public, max-age=3600,ETagи ответ304 Not ModifiedнаIf-None-Match. - Транспорт. HTTPS в продакшене, обычный HTTP только для локальной разработки и ограничение частоты запросов на эндпоинте.
Для статического файла всё это умеет nginx. Учтите: add_header внутри location по умолчанию заменяет унаследованные директивы, а без always пропускается в ответах с ошибкой (документация nginx). Кроме того, статический location отвечает на CORS-preflight OPTIONS кодом 405, поэтому последняя строка возвращает 204.
location = /mcp/server-card {
alias /srv/mcp/server-card.json;
default_type application/mcp-server-card+json;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET" always;
add_header Access-Control-Allow-Headers "Content-Type, If-None-Match" always;
add_header Access-Control-Expose-Headers "ETag" always;
add_header Cache-Control "public, max-age=3600" always;
if ($request_method = OPTIONS) { return 204; }
}
aiSiteReady отдаёт карточку из небольшого маршрута Nitro, чтобы URL следовали за доменом деплоя. До 4 октября 2026 года он выставлял только тип содержимого и кэш на час. С тех пор он следует правилам хостинга из черновика:
MCP_SERVER_CARD_CORS_HEADERSсодержит перечисленные выше заголовки CORS;- кэш живёт час;
ETagпревращает совпавшийIf-None-Matchв304;- клиент, который просит
application/mcp-server-card+json, получает этот медиатип, и у каждого медиатипа свойETag.
CORS-preflight обрабатывает отдельный маршрут для OPTIONS. Это настоящий обработчик в том виде, в каком он был в октябре 2026 года, сокращённый:
export default defineEventHandler((event) => {
const config = useRuntimeConfig(event);
const siteUrl = typeof config.public.siteUrl === "string" ? config.public.siteUrl : "";
const contentType = pickMcpServerCardContentType(getRequestHeader(event, "accept"));
const { body, etag } = getMcpServerCardRepresentation(siteUrl, contentType);
setResponseHeaders(event, MCP_SERVER_CARD_CORS_HEADERS);
setResponseHeader(event, "Cache-Control", MCP_SERVER_CARD_CACHE_CONTROL);
setResponseHeader(event, "ETag", etag);
setResponseHeader(event, "Content-Type", contentType);
setResponseHeader(event, "Vary", "Accept");
if (ifNoneMatchMatches(getRequestHeader(event, "if-none-match"), etag)) {
setResponseStatus(event, 304);
return "";
}
return body;
});
Этот siteUrl важнее, чем кажется, ведь каждый URL в карточке должен быть абсолютным и публичным. Без публичного URL сайта, например на машине разработчика, моя функция buildMcpServerCard подставляет origin dev-сервера, и эндпоинт указывает на localhost с портом 5173.
Сканер пропускает URL из карточки через тот же нормализатор, что и цели сканирования, а он отвергает хосты из одной метки и порты, кроме 80 и 443. Поэтому на localhost проверка достигает максимум PARTIAL; в продакшене, где задан NUXT_PUBLIC_SITE_URL, она проходит.
Карточка хороша ровно настолько, насколько хорош эндпоинт за ней. Настоящий сервер настроен как stateless с ответами в обычном JSON, так что не нужно ни хранить сессию, ни заставлять прокси буферизовать поток событий:
mcp = FastMCP(
name=MCP_SERVER_NAME,
instructions=_MCP_INSTRUCTIONS,
stateless_http=True,
json_response=True,
streamable_http_path="/",
transport_security=build_transport_security(settings),
)
application.mount("/api/mcp", mcp.streamable_http_app())
Последнего аргумента сначала не было. Из-за этого и возникал 421, о котором речь ниже.
Голый /api/mcp отвечает 307 и перенаправляет на /api/mcp/, поэтому карточка указывает вариант со слэшем, и клиенты пропускают редирект.
Каковы пределы безопасности карточки сервера?
Карточка сервера публична, носит справочный характер и не аутентифицирована: в ней не должно быть секретов, она может устареть или оказаться поддельной и не даёт никакого доступа.
- Она публична. Никаких учётных данных, токенов, внутренней топологии и приватных эндпоинтов (discovery.md).
- Она справочная. Устаревшая или поддельная карточка может увести клиента не на тот сервер до того, как он увидит живые значения, поэтому держите её точной.
- Она не несёт аутентификации. Защищённый MCP-сервер обязан реализовать OAuth Protected Resource Metadata (RFC 9728) и указать клиентам на эти метаданные — через
resource_metadataв заголовкеWWW-Authenticateответа401или по well-known URI (авторизация MCP). Полеheadersкарточки лишь описывает заголовки и никаких прав не предоставляет. См. OAuth discovery для ИИ-агентов.
Как карточка сервера связана с карточками A2A, Agent Skills и каталогом API?
Они дополняют друг друга: каждый документ отвечает на один вопрос в своём месте. Карточка говорит, где MCP-сервер; остальные — что ещё вы публикуете, как этим пользоваться и как получить доступ.
| Документ | На какой вопрос отвечает | Расположение | Статус |
|---|---|---|---|
| MCP Server Card | Где MCP-сервер? | Любой URI; рекомендован <mcp-url>/server-card | Черновик, не в IANA |
| AI Catalog | Какие артефакты для агентов публикует домен? | /.well-known/ai-catalog.json | Черновик, не в IANA |
| Карточка агента A2A | Какого агента вы запускаете? | /.well-known/agent-card.json | Зарегистрирована в IANA |
| Каталог API | Какие HTTP API и где их описания? | /.well-known/api-catalog | RFC 9727 |
| Индекс Agent Skills | Каким процедурам следовать агенту? | /.well-known/agent-skills/index.json | Черновик, не в IANA |
| Метаданные защищённого ресурса | Как получить токен? | /.well-known/oauth-protected-resource | RFC 9728 |
Статусы взяты из реестра IANA и из собственной строки статуса предложения Agent Skills. AI Catalog — мостик черновика между карточкой и остальным доменом, а запись для карточки совсем небольшая:
{
"specVersion": "1.0",
"entries": [
{
"identifier": "urn:air:example.com:mcp:weather",
"type": "application/mcp-server-card+json",
"url": "https://api.example.com/mcp/server-card"
}
]
}
О соседях читайте в руководствах по каталогу API, Agent Skills и OAuth discovery.
Какие ошибки ломают MCP Server Card?
Самая опасная ошибка — карточка, указывающая на неработающий эндпоинт: ни один сканер не поймает её снаружи. Большинство сломанных карточек ломаются одним из этих шести способов.
| Ошибка | Что происходит | Как исправить |
|---|---|---|
Карточка указывает эндпоинт, который не работает (404, 405, 421) | Карточка валидна, любое соединение падает | Отправьте на эндпоинт настоящий запрос |
Относительный remotes[].url | Схема черновика требует абсолютный; наша проверка достраивает его до абсолютного и пропускает | Всегда пишите URL с https:// |
localhost, хост из одной метки или порт, отличный от 443, в продакшене | Проверка даёт PARTIAL (mcp_server_card_invalid); приватные IP и внутренние имена хостов всё равно проходят | Задайте публичный URL сайта; прерывайте деплой с ошибкой, если его нет |
Неверный тип содержимого или HTML-заглушка с 200 | PARTIAL (mcp_server_card_wrong_content_type) или карточки как будто нет | Отдавайте JSON; на неопубликованные пути возвращайте 404 |
| Список инструментов или capabilities в карточке | Расходится с живым сервером | Пусть клиенты вызывают tools/list |
| Только старый путь или только новый | Невидима для половины клиентов и сканеров | Публикуйте на /server-card с записью в каталоге; дублируйте на старом пути |
С первой строкой таблицы я столкнулся на собственном деплое, пока писал это руководство. 2 октября 2026 года карточка на www.ai-siteready.com проходила проверку aiSiteReady, но POST tools/list на заявленный эндпоинт вернул 421 Invalid Host header. Именно так отвечает официальный Python SDK, когда включена защита от DNS-rebinding, а заголовка Host нет в списке разрешённых.
SDK (python-sdk 1.x) включает эту защиту автоматически, если FastMCP оставляет хост по умолчанию 127.0.0.1, и тогда пропускает только локальные хосты (исходник python-sdk). Мой build_mcp_server не задавал ни host, ни transport_security, что совпадает.
Исправление попало в продакшен 4 октября 2026 года. Оно явно передаёт TransportSecuritySettings и оставляет защиту включённой, потому что спецификация MCP требует от серверов проверять Origin (Streamable HTTP). Для сайта на www.example.com суть выглядит так:
from mcp.server.transport_security import TransportSecuritySettings
transport_security = TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=["localhost", "localhost:*", "www.example.com", "www.example.com:*"],
allowed_origins=["http://localhost:*", "https://www.example.com"],
)
Каждый хост указан дважды. Шаблон host:* в SDK совпадает только с заголовком Host, в котором есть порт, а nginx с proxy_set_header Host $host передаёт голое имя. Клиенты вне браузера не присылают заголовок Origin, и SDK их за это не отклоняет.
После деплоя initialize и tools/list через публичный прокси ответили 200, чужой Host по-прежнему получал 421, а чужой Origin — 403. Теперь деплой падает, если настоящий initialize, отправленный на публичный URL, не вернул 200 с именем сервера.
Сканер эндпоинт не вызывает, поэтому такую ошибку не поймает; отсюда и следующий раздел.
Как проверить карточку через curl и jq?
Проверяйте три вещи по порядку: заголовки карточки, её поля и эндпоинт, на который она указывает. Ожидайте 200, JSON-тип содержимого и, если вы следуете черновику, CORS и валидаторы кэша.
CARD=https://api.githubcopilot.com/mcp/server-card
curl -sS -D - -o /dev/null -H 'Accept: application/mcp-server-card+json' "$CARD" \
| grep -i -E '^(HTTP|content-type|access-control-allow-origin|cache-control|etag)'
2 октября 2026 года это вывело 200, application/mcp-server-card+json, access-control-allow-origin: *, часовой cache-control и etag. Затем прочитайте поля:
curl -sS "$CARD" | jq '{name, title, remotes}'
{
"name": "io.github.github/github-mcp-server",
"title": "GitHub",
"remotes": [
{
"type": "streamable-http",
"url": "https://api.githubcopilot.com/mcp/"
}
]
}
Наконец, вызовите эндпоинт. Stateless-серверу хватает одного POST; stateful-сервер на старой ревизии возвращает Mcp-Session-Id из initialize, и его нужно передавать дальше. В 2026-07-28 initialize нет: первый запрос несёт версию протокола в заголовке и в _meta. Этот запрос повторяет форму server/discover из спецификации; на живом сервере 2026-07-28 я его не запускал.
URL=$(curl -sS "$CARD" | jq -r '.remotes[0].url')
curl -sS -X POST "$URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
Ожидайте JSON-RPC-результат с supportedVersions или UnsupportedProtocolVersionError со списком версий, которые сервер всё же поддерживает (server/discover).
Затем перенесите процедуру в CI. Скрипт считает 200, 400 и 401 признаком «эндпоинт жив» (защищённый сервер отвечает 401, а сервер только новой эпохи может ответить 400 на голый tools/list), а любой другой статус, например 404, 405, 421 или 5xx, — отказом. В октябре 2026 года карточка GitHub прошла этот скрипт. До 4 октября наша не проходила проверку полей, потому что была в старой форме, а следующий шаг поймал бы 421 на эндпоинте. После обоих исправлений проходит и наша.
#!/usr/bin/env bash
set -euo pipefail
CARD="${1:?usage: check-card.sh https://api.example.com/mcp/server-card}"
HEADERS="$(curl -sS -D - -o /dev/null -H 'Accept: application/mcp-server-card+json, application/json' "$CARD")"
grep -qE '^HTTP/[0-9.]+ 200' <<<"$HEADERS" || { echo 'FAIL: card did not return 200'; exit 1; }
grep -qiE '^content-type: application/(mcp-server-card\+)?json' <<<"$HEADERS" \
|| { echo 'FAIL: Content-Type is not a JSON media type'; exit 1; }
BODY="$(curl -sS "$CARD")"
jq -e '(.["$schema"] | type == "string")
and (.name | type == "string" and test("^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$"))
and (.version | type == "string")
and (.description | type == "string" and length >= 1 and length <= 100)' <<<"$BODY" >/dev/null \
|| { echo 'FAIL: $schema, name, version or description is missing or invalid'; exit 1; }
jq -e '[.remotes[]?.url] | length > 0 and all(test("^https://"))' <<<"$BODY" >/dev/null \
|| { echo 'FAIL: remotes[].url must be an absolute https URL'; exit 1; }
URL="$(jq -r '.remotes[0].url' <<<"$BODY")"
CODE="$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}')"
case "$CODE" in
200|400|401) echo "OK: card is well-formed, endpoint answered $CODE" ;;
*) echo "FAIL: endpoint $URL answered $CODE"; exit 1 ;;
esac
Как aiSiteReady проверяет карточку MCP-сервера?
По состоянию на октябрь 2026 года проверка mcp_server_card в aiSiteReady, входящая в категорию Protocol/API, ищет карточку и там, где её размещает черновик, и по старым well-known-путям и проверяет её форму и URL, не обращаясь к эндпоинту.
Она запрашивает пять well-known-путей (/.well-known/mcp, /.well-known/mcp/server-card.json, /.well-known/mcp.json, /.well-known/mcp-server.json, /.well-known/mcp/server.json) и читает заголовок Link корневого документа в поисках значений rel, равных mcp-server и mcp-server-card — это собственная терпимость сканера, а не то, что я нашёл в черновиках MCP. Кроме того, она считает просканированный URL адресом MCP и запрашивает его с добавленным /server-card. Затем читает /.well-known/ai-catalog.json: берёт встроенные карточки и загружает до пяти записей application/mcp-server-card+json. Поэтому при сканировании https://example.com/ проверяется https://example.com/server-card. Карточку по адресу https://api.example.com/mcp/server-card проверка найдёт через каталог или если сканировать сам https://api.example.com/mcp.
Запросы идут через SSRF-безопасный fetcher, JavaScript не выполняется, а URL эндпоинтов из карточки не запрашиваются никогда.
PASS — валидная карточка без мягких замечаний (soft issues). PARTIAL — карточка есть, но неполная, отдана с неверным типом содержимого, у неё нечитаемая как дата protocolVersion или запись в supportedProtocolVersions, или в ней URL эндпоинта, который отвергает нормализатор URL сканера, например хост из одной метки вроде localhost или порт, отличный от 80 и 443. Шаблоны черновика с {variable} принимаются как есть. UNKNOWN — блокировка бота или сбой запроса помешали прочитать ответ. FAIL — карточка не найдена.
PASS, PARTIAL и FAIL дают 1,0, 0,5 и 0 при средней серьёзности; вся категория отключена в профиле Контентный сайт и весит больше в профиле API/приложение. Веса описаны на странице методологии, а 30-минутный аудит готовности показывает, где эта проверка стоит в полном проходе.
Запустите бесплатное сканирование, чтобы увидеть, в какой форме ваша карточка, являются ли URL внутри неё корректными абсолютными URL и какие исправления делать первыми, — на английском, украинском или русском.
Если коротко: MCP Server Card — это публичный справочный указатель, который говорит, где ваш сервер и какие версии он поддерживает, и дальше ни во что не вмешивается. Пока это черновик, так что публикуйте карточку там, где указывает черновик, дублируйте там, где смотрят сканеры, держите каждый URL абсолютным и публичным и проверяйте эндпоинт за ней настоящим запросом, потому что больше его никто не проверит.
У IMozz 20 лет в разработке ПО, а последний год он строит продукты с помощью LLM. Он развивает aiSiteReady, сканер только для чтения, который проверяет, может ли ИИ-агент прочитать и вызвать сайт. Сканер серверно рендерит собственный контент как рабочий пример.