Implementing SEP-2127 server cards on an 810-pack gateway
What we shipped, the two ambiguities we flagged, and the path-location refactor that landed in experimental-ext-server-card#22.
Update 2026-06-09: the canonical server-card path moved between the SEP body and the new Extensions-Track wire-format repo. Per experimental-ext-server-card#22 (merged 2026-06-08), the recommended location is now
GET <streamable-http-url>/server-card, NOT/.well-known/mcp-server-card. The discovery entry point is/.well-known/mcp/catalog.json. Both v1 paths still 301 to the new v2 ones on our gateway, so anything you bookmarked still works — but if you’re implementing fresh, use the v2 paths below.
The MCP Server Card Working Group is shipping a discovery spec — originally SEP-2127, now refactored to the Extensions Track as PR #2893 with the wire format living in experimental-ext-server-card. It defines a discoverable endpoint that lets clients see a server’s identity, transports, and protocol versions before opening a connection — instead of completing a full initialize handshake just to see the name.
Pipeworx is the textbook multi-server-host case: 810 packs served from one origin at gateway.pipeworx.io. We shipped the original .well-known form on 2026-06-05, then migrated to the new /server-card form on 2026-06-09 when ext-server-card#22 merged. This post covers what we shipped, the two ambiguities we hit (both upstream-tracked now), and the path migration.
What’s live now (v2)
Five paths against the gateway:
GET /mcp/server-card # host card (io.pipeworx/gateway)
GET /{slug}/mcp/server-card # per-pack card — 810 of them
GET /.well-known/mcp/catalog.json # discovery: full list of card URLs
GET /<bad>/mcp/server-card # 404 with a discovery hint
GET /.well-known/mcp-server-card[/…] # 301 → v2 path (v1 backward compat)
The v1 path-prefix is a 301 alias so anyone who adopted the earlier shape — including SkinKnowledgeBase and SIMOSphere AI — keeps working through the transition.
Each card follows the schema: $schema, name (reverse-DNS, one slash), version, title, description, websiteUrl, repository, icons, remotes (with headers[] for BYO-key packs), and _meta. Primitives are omitted per the spec — tools vary with the authenticated session, so clients still issue tools/list at runtime via the protocol.
Try it:
# v2 (canonical)
curl https://gateway.pipeworx.io/mcp/server-card
curl https://gateway.pipeworx.io/edgar/mcp/server-card
curl https://gateway.pipeworx.io/.well-known/mcp/catalog.json | jq '.mcp_servers | length'
# v1 (still works — 301 redirects to v2)
curl -L https://gateway.pipeworx.io/.well-known/mcp-server-card
curl -L https://gateway.pipeworx.io/.well-known/mcp-server-card/edgar
curl https://gateway.pipeworx.io/.well-known/mcp-server-card/attom
The attom card advertises its _apiKey as a non-required secret via the remotes[0].headers[] array. The edgar card has no headers section — it’s a keyless public-data pack.
What was unambiguous and easy
- Schema as a strict subset of
server.json. All the data already existed for every pack (slug, version, the remote URL, the BYO-key posture). No new metadata to source. - Path collisions. Our existing
.well-known/oauth-*routes didn’t conflict —mcp-server-cardis its own segment. - CORS + cache headers. One-liner each, both required by the spec.
Two ambiguities we hit (both now upstream-tracked)
{server-name} matching when name contains a slash
The schema requires name to be reverse-DNS with exactly one forward slash separating namespace from name (io.pipeworx/edgar), and the original SEP defined the per-server path as /.well-known/mcp-server-card/{server-name}. URLs can’t carry unencoded slashes, so we matched on the post-slash segment: io.pipeworx/edgar was reachable at /.well-known/mcp-server-card/edgar. Alex Akimov flagged the ambiguity in his doc review.
Mostly moot now — the path-location refactor in #22 sidesteps the issue (per-pack cards live at /{slug}/mcp/server-card, which is unambiguous), but the same matching question still applies if any client tries to look up a card by reverse-DNS name. Worth pinning in the new wire-format doc.
headers[] semantics for tool-argument auth
Many of our packs accept an API key passed as a tool argument (_apiKey) rather than as an HTTP header. We used the remotes[].headers[] array to advertise it with isSecret: true and isRequired: false, but it’s technically not an HTTP header. For us this matters for ~150 BYO-key packs. A parallel toolArguments[] field, or an explicit note in the headers[] description about supporting tool-arg auth, would remove the ambiguity.
The path-location refactor (#22)
Between v1 and v2 the canonical location moved. The argument (from Darrel Miller, formalized in experimental-ext-server-card#22) is that .well-known is for site-wide metadata (one OAuth issuer per origin, one robots policy per origin), not for application-level metadata like an individual MCP server card. The fix:
- Single-server card lives at
<streamable-http-url>/server-card. Predictable per-server location; multi-server hosts get distinct paths for free. - Discovery via
.well-known/mcp/catalog.json. A list of card URLs on the host. Clients hit one well-known endpoint to enumerate; they never have to guess where a card lives.
Pipeworx now serves both. The v1 paths (.well-known/mcp-server-card[/…]) 301 to their v2 equivalents — so any client that adopted v1 (including SkinKnowledgeBase, SIMOSphere AI, and our own earlier blog readers) keeps working.
_meta extensions are forming an ecosystem
The spec correctly excludes most extension surfaces — pricing, quality, SLA, payment, context payloads — from the core card. They all naturally ride as reverse-DNS _meta keys instead. In one week we’ve seen three independent prototypes hit this pattern:
- ASM (Agent Service Manifest): pricing / quality / SLA / invocability. Rides at
_meta.io.modelcontextprotocol.registry/publisher-provided.asm. - mcp-pay (dipankar): payment metadata.
- faf-server-card-ref (Wolfe-Jam): project context payloads via
application/vnd.faf+yaml.
We don’t ship any of these today, but the _meta rider pattern is the right one — additive, no spec fork, independent iteration. Worth tracking which of these get adopters; first one with traction probably becomes the de facto standard.
What’s deliberately not in the card
- Primitive lists. The spec correctly excludes tools/resources/prompts. They vary with session authentication; a static document can’t represent them correctly.
tools/listat runtime is the right answer. Confirmed by experimental-ext-server-card#23 (closed 2026-06-08) which adds a normative requirement that cards MUST NOT contradict runtime — we comply naturally because our cards are generated from the same source the runtime initialize handler reads. - A “default-pack” pointer for multi-server hosts. The catalog endpoint solves the discovery half; the “which-of-810-do-I-pick” half is what ASM’s invocability axis is trying to solve.
Why bother
For Pipeworx, three concrete reasons:
- Pre-connection discovery. Clients can see our 810-pack catalog and identity before opening a session. Matters for autoconfig, IDE-side install UX, and registry indexing.
- Per-pack discoverability. Each of our 810 packs has its own stable, machine-readable identity document. Linked from the catalog and from the host card’s
_meta.io.pipeworx/per_pack_card_pattern. - Spec influence. Being one of the first production implementations during the WG’s active drafting phase gives our feedback weight on open ambiguities. Both of ours are now upstream-tracked.
The spec is still open. If you’re building an MCP server today, use the v2 path: GET <your-streamable-http-url>/server-card for the card, GET /.well-known/mcp/catalog.json for the discovery list. Comment on PR #2127 or file an issue on experimental-ext-server-card if you hit an ambiguity — both repos are actively reviewed.