Skip to content

Add MCP Server Card (SEP-2127) types + handler#2768

Draft
SamMorrowDrums wants to merge 4 commits into
mainfrom
sammorrowdrums-server-card-handler
Draft

Add MCP Server Card (SEP-2127) types + handler#2768
SamMorrowDrums wants to merge 4 commits into
mainfrom
sammorrowdrums-server-card-handler

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

What

Makes the GitHub MCP Server discoverable via an MCP Server Card (SEP-2127). Adds a new pkg/http/servercard package with:

  1. Go types matching modelcontextprotocol/experimental-ext-server-card schema.jsonServerCard, Remote, Repository, Icon, Input, KeyValueInput. Remote-only (no packages — those stay in the registry server.json).
  2. A constructor (NewServerCard) for the GitHub MCP Server card, reusing the identity fields from the existing static server.json:
    • name io.github.github/github-mcp-server, title GitHub, description (reused, exactly 100 chars)
    • version wired from the build version var
    • repository { url, source: "github", id: "942771284" }
    • one streamable-http remote, default https://api.githubcopilot.com/mcp/ (per-environment host left to the remote repo via Config.RemoteURL)
    • $schema = https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json
  3. A public, no-auth http.Handler serving the card as application/mcp-server-card+json with read-only CORS (Allow-Origin *, Allow-Methods GET, Allow-Headers Content-Type), Cache-Control: public, max-age=3600, and Accept content negotiation. It mirrors the existing OAuth Protected-Resource-Metadata handler (pkg/http/oauth) so the remote repo can mount it identically and wire env-specific hosts.
  4. Table-driven httptest tests validating emitted cards against the canonical experimental-ext-server-card JSON Schema (embedded in testdata/) plus handler headers, media type, and method handling.

The handler is mounted at the reserved <streamable-http-url>/server-card location — both /server-card and /mcp/server-card (GitHub's edge strips /mcp) — and wired into the local HTTP server alongside the OAuth metadata routes.

ETag + conditional requests

The card response now carries a strong ETag and honors conditional requests, matching a contract shared byte-for-byte across our implementations (proposed upstream as a SHOULD in experimental-ext-server-card#33):

  • Strong ETag = SHA-256 of the exact served body bytes, lowercase hex, double-quoted (deterministic for identical content).
  • GET and HEAD always set ETag alongside Content-Type, CORS, and Cache-Control: public, max-age=3600.
  • If-None-Match matching the strong tag, its weak W/"..." form, or *304 Not Modified with ETag + Cache-Control and an empty body (RFC 9110 weak comparison). Otherwise 200 + full body.
  • OPTIONS preflight and 405-for-other-methods are unchanged.

Reusable, request-aware serving for multi-tenant remotes

To let the hosted (multi-tenant) remote server reuse identical ETag/header logic when the card URL varies per request (proxima is multi-tenant; URL derives from X-Forwarded-Host), two reuse points are exposed:

  • func ServeCard(w http.ResponseWriter, r *http.Request, card *ServerCard) — package-level canonical response writer (single source of truth for headers + conditional handling) for callers that build a card per request.
  • Config.RemoteURLFunc func(*http.Request) string — per-request remote-URL deriver consumed by the Handler (recommended for proxima — the remote supplies only the URL deriver and all serving logic stays here).

Design decision (flagged)

Types + handler live in OSS here, mirroring the OAuth PRM pattern, rather than entirely in the remote repo. This keeps the card's identity/version consistent with runtime serverInfo at the source and lets the hosted/remote server consume servercard.NewHandler(...).RegisterRoutes(...) with a per-environment RemoteURL.

Validation

  • go build ./..., go test -race ./..., and golangci-lint all pass
  • Emitted JSON validated against schema.json (in unit tests, via jsonschema-go)
  • Live smoke test confirms GET → 200 + correct headers, OPTIONS → 200 preflight, HEAD → 200, POST → 405, incompatible Accept → 406, and no impact to existing MCP / OAuth routes
  • Live smoke test confirms ETag conditional behavior: GET returns a stable quoted strong ETag; matching / weak-form / wildcard If-None-Match304 empty body; non-matching → 200 + body

Refs

Draft — do not merge.

SamMorrowDrums and others added 4 commits June 25, 2026 12:05
Introduce a `servercard` package that defines the GitHub MCP Server's
Server Card (SEP-2127) and a public, no-auth HTTP handler that serves it.

The Server Card is a static, remote-only discovery document: it reuses the
identity fields from the registry `server.json` (name, title, description,
repository) and advertises a single streamable-http remote (default
https://api.githubcopilot.com/mcp/, overridable per environment). Following
the spec it omits installable packages, which stay in the registry document.

The handler mirrors the OAuth protected-resource-metadata handler: it serves
`application/mcp-server-card+json` with read-only CORS, a one-hour
Cache-Control, Accept content negotiation, and no authentication. It is
registered at the reserved `<streamable-http-url>/server-card` location (both
`/server-card` and `/mcp/server-card`) so the remote server repository can
mount it identically.

Tests validate emitted cards against the canonical
experimental-ext-server-card JSON Schema and cover the handler's headers,
media type, and method handling.

Refs github/copilot-mcp-core#1855, github/copilot-mcp-core#1853

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add strong ETag (SHA-256 of the served body) and RFC 9110 If-None-Match
handling to the Server Card response: GET and HEAD always emit ETag
alongside Content-Type, CORS, and Cache-Control, and a matching
If-None-Match (strong, weak W/ form, or *) returns 304 Not Modified with
ETag + Cache-Control and an empty body.

Expose the serving logic two ways so the multi-tenant remote can reuse
byte-for-byte identical ETag/header behavior:
- ServeCard(w, r, *ServerCard): package-level canonical response writer
  for callers that build a card per request.
- Config.RemoteURLFunc func(*http.Request) string: per-request remote URL
  deriver (recommended for proxima) consumed by the Handler.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Populate the optional forward fields the v1 Server Card schema already
defines so the card is complete for discovery:

- icons: the GitHub mark in light and dark themes, reusing the embedded
  Octicons as self-contained data URIs so the card has no external image
  dependency. The icon order is fixed to keep the serialized card — and
  thus its ETag — deterministic.
- supportedProtocolVersions on the streamable-http remote, defaulted from
  DefaultProtocolVersions (mirroring the bundled go-sdk's supported
  versions, which are unexported) and overridable via Config.

The canonical card name io.github.github/github-mcp-server is unchanged,
preserving the downstream AI Catalog identity derived from it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The card is discovered via a single catalog link, so it must be served at
exactly one canonical location. Register only the reserved relative path
/server-card and drop the duplicate /mcp/server-card registration: the
hosted edge strips the /mcp base path before forwarding, so the backend
receives /server-card while the public URL stays
https://api.githubcopilot.com/mcp/server-card.

Update the tests to assert the single canonical path, guard that
/mcp/server-card is no longer registered (404), and verify the static
card route is not shadowed by the streamable MCP catch-all mount.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant