Перейти до основного вмісту
БЛОГ

MCP Server Card: шлях, схема й перевірка картки

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

Автор: IMozzОновлено 2026-10-23
Картка MCP-сервера: розбір — aiSiteReady

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[]Саме з'єднання
Які версії протоколу?Підказка, supportedProtocolVersionsserver/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 у картці показує, з якою епохою клієнт зустрінеться.

Виявлення, потім підключення: запитати AI Catalog і картку за URL MCP плюс /server-card, обрати remote, потім server/discover або initialize і tools/list.

Коли я робив картку для 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 (жовтень 2025) і SEP-1960 (грудень 2025) використовували .well-known і закриті; SEP-2127 рекомендує URL MCP плюс /server-card.

У самому 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Так, усередині remotestreamable-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.

Будова MCP Server Card: чотири обов'язкові поля й масив remotes з type, url і версіями протоколу. Інструментів, capabilities та облікових даних у ній немає.

Там, де чернетка й сканер розходяться, перевірка м'якша до вмісту й ширша за шляхами. Станом на жовтень 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-catalogRFC 9727
Індекс Agent SkillsЯких процедур має дотримуватися агент?/.well-known/agent-skills/index.jsonЧернетка, не в IANA
Метадані захищеного ресурсуЯк отримати токен?/.well-known/oauth-protected-resourceRFC 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-заглушка з 200PARTIAL (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, сканер лише для читання, що перевіряє, чи може ШІ-агент прочитати й викликати сайт. Сканер серверно рендерить власний контент як робочий приклад.

Часті запитання

Чи це офіційне розташування картки MCP-сервера — /.well-known/mcp/server-card.json?
Ні. Цей шлях прийшов із SEP-1649, який закрили в січні 2026 року й продовжили як SEP-2127. Поточна чернетка дозволяє тримати картку за будь-яким незарезервованим URI, рекомендує URL MCP-ендпоінта плюс /server-card і очікує, що клієнти знайдуть її через AI Catalog за адресою /.well-known/ai-catalog.json. Частина сканерів досі опитує старий шлях, тож дублювати картку й за цим шляхом — розумний компроміс.
Чи замінює картка MCP-сервера initialize або tools/list?
Ні. Картка містить лише ідентифікаційні дані, віддалені ендпоінти та підтримувані версії протоколу. Інструменти, ресурси, промпти й capabilities лишаються в живих викликах протоколу на кшталт tools/list, а чернетка велить клієнтам за розбіжності довіряти живим значенням. У ревізії 2026-07-28 initialize немає: сервери зобов'язані реалізувати server/discover, а клієнт може викликати його першим. Раніші ревізії починають з initialize.
Які поля обов'язкові в картці MCP-сервера?
За поточною схемою чернетки — чотири: $schema, name, version і description. Ім'я пишуть у форматі reverse-DNS рівно з одним слешем, наприклад com.example/weather, а опис займає від 1 до 100 символів. Масив remotes у схемі необов'язковий, але без remote з type і url клієнтові просто нема до чого підключатися.
Чи можна додавати до картки облікові дані або список інструментів?
Не можна ні те, ні інше. Картки публічні за задумом і не повинні містити облікові дані, токени чи внутрішні ендпоінти. Чернетка також виключає інструменти, ресурси й промпти, бо статичний список розходиться з тим, що сервер повертає в живій автентифікованій сесії. Вважайте кожне поле картки довідковим і ніколи не використовуйте його як підставу для рішень про доступ.