Server Card Check
An independent checker for MCP Server Cards (SEP-2127). Paste a server's URL and see, rule by rule, what passes and what to fix.
Try it online · What it checks · Census
npx mcp-server-card-check https://your-server.example.com/mcp
A Server Card is a small public file that tells MCP clients who a server is and how to connect, before they connect. SEP-2127 made it Final on 2026-10-06. Every result here says MUST or SHOULD, pass or fail, why, and which section of the spec it comes from.
What you can do
- Check your server in the browser at servercard.wolfejam.dev. Usually a few seconds; nothing to install.
- Fail the build on a broken card: add
npx mcp-server-card-check https://your-server/mcp to CI. It exits 1 on any MUST failure.
- Check a server before you trust it: run it on someone else's endpoint before adding it to your client or catalog.
- Compare the card with the live server: add
--transport to connect once and check that the server says what its card says.
Did you know?
From a census of every MCP Registry remote server on 2026-10-12, six days after the spec merged:
- About 2% publish a valid Server Card so far.
- 637 of the 3,609 endpoints that answer at
/server-card return a web page, not a card. A 200 isn't a card.
- Only 6% of cards send all the CORS headers browsers need to cache and refresh them.
- A card can pass the schema and still be wrong: invented protocol versions, or a name that doesn't match its registry entry. The schema doesn't catch either; this checker does.
Use it
| How | Command |
|---|
| Once, no install | npx mcp-server-card-check <mcp-url> |
| In CI | npx mcp-server-card-check <mcp-url> --json (exit code 1 on any MUST failure) |
| Against the live server | npx mcp-server-card-check <mcp-url> --transport (connects once: the transport and consistency checks) |
| With your registry name | npx mcp-server-card-check <mcp-url> --registry-name com.example/weather |
| In the browser | servercard.wolfejam.dev: the same checks, read-only, nothing to install |
Read-only by default. It sends GET and OPTIONS requests only. The two transport checks (a foreign Origin is refused; a local server refuses a foreign Host) and the six consistency checks send a POST, so they run only with --transport.
Options: --json · --registry-name NAME · --card-url URL · --catalog-url URL · --timeout MS (default 10000) · --transport · -V / --version · -h / --help.
Exit codes: 0 no MUST failures · 1 at least one MUST failure · 2 usage error, or nothing answered.
Node 22 or later. The package installs two commands, server-card-check and mcp-server-card-check; they are the same tool.
Example output (abridged):
server-card-check 0.4.1 · https://your-server.example.com/mcp
card: https://your-server.example.com/mcp/server-card · catalog: https://your-server.example.com/.well-known/ai-catalog.json
CARD
✓ MUST The card validates against the v1 JSON Schema
✗ SHOULD supportedProtocolVersions lists released MCP protocol versions
not MCP versions: 2025-03-12 (released: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, 2026-07-28)
v1 schema: remotes[].supportedProtocolVersions + MCP released versions
HOSTING
✗ MUST CORS: ETag is exposed
got ""
discovery.md § CORS Requirements
…
29 passed · 2 failed · 8 skipped · 1 MUST failure
As a library
import { checkDiscovery } from "mcp-server-card-check";
import { cardValidator } from "mcp-server-card-check/validate";
const report = await checkDiscovery(
{ mcpUrl: "https://your-server.example.com/mcp", registryName: "com.example/weather" },
{ validateCard: cardValidator() },
);
console.log(report.mustFailures, report.results);
checkDiscovery takes an optional fetch, so tests and other runtimes can supply their own.
In a browser extension or a Worker, import from mcp-server-card-check/browser. It has the same checker plus a precompiled validateCard, because those runtimes forbid compiling the schema at run time. For a read-only run, pass a fetch that refuses POST and rawStatus: async () => null; the transport and consistency checks then report "skip".
import { checkDiscovery, validateCard } from "mcp-server-card-check/browser";
const readOnly: typeof fetch = (url, init) =>
(init?.method ?? "GET") === "POST" ? Promise.reject(new Error("read-only")) : fetch(url, init);
const report = await checkDiscovery({ mcpUrl }, { validateCard, fetch: readOnly, rawStatus: async () => null });
What it checks
The card is validated against the v1 Server Card schema at ext-server-card@526201bb, the snapshot SEP-2127 pins. Levels follow the spec's wording; where the spec is soft, the check is a SHOULD.
Notes on six checks:
card.no-secrets looks for credential values: a value under a secret-named key, known token shapes, Bearer <token> where a header value would go, a pre-set value on an isSecret input. Placeholders ({token}, <key>, YOUR_API_KEY, xxxx) and prose that mentions auth pass.
card.protocol-versions fails values that are not dates, and dates on or before the newest released MCP version that were never released. Newer dates pass, so a future MCP release doesn't make the checker wrong.
consistency.* connect once: server/discover (the 2026-07-28 spec says servers MUST implement it), or initialize for a legacy server; any session is closed afterwards. They check that the card doesn't contradict what the server says once connected, as discovery.md asks ("SHOULD NOT contradict"). The report says which method answered. name is compared only when serverInfo.name is itself reverse-DNS: a short name can't contradict the card's. A server that needs authorization (401/403) is skipped, not failed.
hosting.cors-*: discovery.md says hosted cards MUST include "appropriate CORS headers" and lists four. Read against the Fetch standard: Access-Control-Allow-Origin and Expose-Headers on the GET response; Allow-Origin, Allow-Methods and Allow-Headers on the preflight (OPTIONS) response, where browsers read them. A client sending If-None-Match (as § Caching asks) always preflights, since it isn't a CORS-safelisted header. * counts for Allow-Methods, Allow-Headers and Expose-Headers (requests without credentials). The preflight must answer with a 2xx status and an Allow-Origin of * or the request's origin. A header sent on the GET only fails, and the detail says so.
transport.origin-403 is a MUST for a local server and a SHOULD for a remote one. The spec says servers MUST answer an "invalid" Origin with 403 to prevent DNS rebinding, but leaves "invalid" to the server, and the attack it names is on local servers. A public server can accept every origin by design.
card.registry-name runs only with --registry-name. SEP-2127 says cards and the registry's server.json "use compatible metadata where practical", identity included.
Census
docs/reference.md records a read-only census of every MCP Registry remote server, run with this checker on 2026-10-12, and how to repeat it.
Contributing
Think a verdict is wrong? Open a "Wrong verdict" issue with the URL and the check. The spec text decides. See CONTRIBUTING.md.
License
MIT