Skip to content

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

Open
SamMorrowDrums wants to merge 1 commit into
mainfrom
sammorrowdrums-server-card-handler
Open

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

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

What & why

Makes the GitHub MCP Server discoverable via an MCP Server Card. Adds a new OSS-owned package pkg/http/servercard with:

  • Go types matching the current v1 Server Card schema in modelcontextprotocol/experimental-ext-server-card (schema.json + docs/discovery.md).
  • A constructor (NewServerCard) for the GitHub MCP Server's card, reusing the identity fields from the registry server.json so both documents describe the same server.
  • A public, no-auth, read-only http.Handler that serves the card at the reserved /server-card location (public URL: transport URL + /server-card, e.g. https://api.githubcopilot.com/mcp/server-card).

Ownership: this package owns the Server Card implementation and all stable GitHub MCP identity/metadata, but the card is mounted and hosted only by the remote deployment (github/github-mcp-server-remote), which supplies the environment-specific remote URL and reuses this serving logic verbatim. The standalone OSS server does not advertise the card, so self-hosted binaries never falsely claim the dotcom remote.

Note: SEP-2127 is still open/in review — this tracks the current canonical v1 schema + discovery rules in the experimental extension repo, not an accepted core extension.

Card shape (remote-only, minimal)

The card is deliberately remote-only and minimal — it advertises identity + a single streamable-http remote and omits tools/resources/prompts and installable packages (those stay in the registry server.json). This is the exact JSON emitted for github.com:

{
  "$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
  "name": "io.github.github/github-mcp-server",
  "version": "<build version>",
  "description": "Connect AI assistants to GitHub - manage repos, issues, PRs, and workflows through natural language.",
  "title": "GitHub",
  "websiteUrl": "https://github.com/github/github-mcp-server",
  "repository": { "url": "https://github.com/github/github-mcp-server", "source": "github", "id": "942771284" },
  "remotes": [
    { "type": "streamable-http", "url": "https://api.githubcopilot.com/mcp/" }
  ]
}

The name is locked to io.github.github/github-mcp-server, matching the registry server.json. It is the stable Server Card / registry server identity; the AI Catalog identifier is assigned independently and is not derived from the card name.

Serving behavior (per discovery.md)

  • Media type application/mcp-server-card+json with RFC 9110 Accept negotiation, including quality values — an explicit q=0 on the most specific matching media range rejects with 406 Not Acceptable.
  • CORS — the mandated four headers, applied uniformly to GET/HEAD/OPTIONS/304:
    • Access-Control-Allow-Origin: *
    • Access-Control-Allow-Methods: GET
    • Access-Control-Allow-Headers: Content-Type, If-None-Match
    • Access-Control-Expose-Headers: ETag
  • Cache-Control: public, max-age=3600.
  • Strong ETag (SHA-256 of the exact served body, quoted lowercase hex) with If-None-Match304 Not Modified (empty body). Weak comparison per RFC 9110. See upstream ETag proposal experimental-ext-server-card#33.

RegisterRoutes mounts the card at the single reserved /server-card path. A composition test guards that route against being shadowed by an MCP catch-all mount (r.Mount("/", h)), so the remote can register both on one router.

Reuse by the hosted/remote deployment

The hosted deployment is multi-tenant, so the remote URL (and therefore the card body + ETag) varies per request. To keep header/ETag logic byte-for-byte identical across OSS and the remote, the handler exposes:

  • Config.RemoteURLFunc func(*http.Request) string — derive the per-request remote URL.
  • ServeCard(w, r, card *ServerCard) — write the full response (Content-Type, CORS, Cache-Control, ETag, If-None-Match/304) for an already-built card.

Deliberately omitted (kept minimal)

  • Installable packages — remote-only card; packages live in the registry server.json.
  • supportedProtocolVersions — the go-sdk does not export the versions it negotiates, so it cannot be advertised accurately from the runtime; a hand-maintained list would drift.
  • Iconstitle / description / repository / websiteUrl are sufficient for discovery; icon data-URIs added code without contract value.
  • Per-remote auth metadata — the hosted server advertises auth via OAuth protected-resource-metadata discovery; duplicating it on the card would risk drift and cannot capture every accepted mode. The v1 schema only requires type + url on a remote.

Validation

  • go build ./..., go test -race ./... (full suite), ./script/lint (0 issues) — all green.
  • Diff kept to 780 additions across 4 files (package + tests only); no vendored schema copy is checked in, and the standalone OSS server is untouched.
  • Release-quality history: rebased onto current main (== v1.11.0) and squashed into a single commit.

Refs: github/copilot-mcp-core#1855 · epic github/copilot-mcp-core#1853

@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-server-card-handler branch 2 times, most recently from eaa898f to 06a878c Compare August 26, 2026 10:34
@SamMorrowDrums
SamMorrowDrums requested a balanced review from Copilot August 26, 2026 10:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds MCP Server Card discovery support for the GitHub MCP Server.

Changes:

  • Defines Server Card types and metadata.
  • Adds HTTP serving, CORS, caching, and ETag handling.
  • Integrates and tests the public /server-card route.
Show a summary per file
File Description
pkg/http/servercard/card.go Defines card types and construction.
pkg/http/servercard/card_test.go Tests card metadata and serialization.
pkg/http/servercard/handler.go Implements HTTP serving and negotiation.
pkg/http/servercard/handler_test.go Tests handler behavior and caching.
pkg/http/server.go Registers the card endpoint.
pkg/http/server_test.go Tests router and CORS isolation.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 6/6 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment on lines +104 to +107
// RemoteURLFunc, when set, derives the streamable-HTTP remote URL from the
// incoming request, taking precedence over RemoteURL whenever it returns a
// non-empty value. This supports multi-tenant deployments (e.g. proxima)
// where the absolute URL varies per request (e.g. from X-Forwarded-Host).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting the constraint rather than changing behavior here. In the hosted topology each tenant is served under a distinct public hostname, so public, max-age=3600 in a shared/CDN cache keys on that hostname and can't cross-serve tenants; X-Forwarded-Host is an internal edge→origin detail the origin can't safely Vary on. The discovery spec also mandates public, max-age=3600, and this handler can't know which header an opaque RemoteURLFunc consulted. So a request-header Vary belongs at the edge/deployment layer, not this handler. Leaving open for reviewer visibility.

Comment thread pkg/http/servercard/card.go
Comment thread pkg/http/servercard/handler.go
Comment thread pkg/http/servercard/handler.go Outdated
@SamMorrowDrums
SamMorrowDrums requested a balanced review from Copilot August 26, 2026 10:39
@SamMorrowDrums
SamMorrowDrums marked this pull request as ready for review August 26, 2026 10:40
@SamMorrowDrums
SamMorrowDrums requested a review from a team as a code owner August 26, 2026 10:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

pkg/http/servercard/handler.go:181

  • The Accept parser discards every parameter, so Accept: application/mcp-server-card+json;q=0 (and application/*;q=0) is treated as acceptable even though a zero quality value explicitly rejects that representation. Parse the media ranges and their quality weights, including specificity precedence, and return 406 when the effective quality for MediaType is zero.
		if i := strings.IndexByte(mediaRange, ';'); i >= 0 {
			mediaRange = strings.TrimSpace(mediaRange[:i])
		}
		switch strings.ToLower(mediaRange) {
		case MediaType, "*/*", "application/*":
			return true

pkg/http/servercard/handler.go:63

  • Because the handler selects between 200 and 406 based on Accept and the 200 response is publicly cacheable, it needs Vary: Accept. Without it, a shared cache may replay a cached card to a request that explicitly rejects this media type (or otherwise reuse the negotiated response incorrectly).
	if !acceptsCard(r.Header.Get(headers.AcceptHeader)) {
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/http/servercard/card.go Outdated
Comment thread pkg/http/server.go Outdated
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-server-card-handler branch 2 times, most recently from 273b365 to e33dc64 Compare August 26, 2026 10:52
Introduce pkg/http/servercard: Go types for an MCP Server Card matching the
current v1 schema in modelcontextprotocol/experimental-ext-server-card, a
constructor for the GitHub MCP Server's card, and a public, no-auth HTTP
handler that serves it at the reserved /server-card location.

The card is remote-only and minimal: it advertises identity (name, title,
description, version), repository, websiteUrl, and a single streamable-http
remote, and deliberately omits tools/resources/prompts and installable
packages. Card identity fields are reused from the registry server.json so
both documents describe the same server. The card name is the stable server
identity only; the AI Catalog identifier is assigned independently and is not
derived from it.

Serving behavior follows the discovery spec: media type
application/mcp-server-card+json, RFC 9110 Accept negotiation (including q=0,
which rejects with 406), the mandated four CORS headers (Allow-Origin *,
Allow-Methods GET, Allow-Headers Content-Type, If-None-Match, Expose-Headers
ETag), Cache-Control public max-age=3600, and a strong SHA-256 ETag with
If-None-Match/304 conditional handling. Vary: Accept is set so shared caches
key on the negotiated media type.

To support the multi-tenant hosted deployment, the handler exposes a
request-aware ServeCard helper and a Config.RemoteURLFunc hook so the remote
repository can derive a per-request remote URL while reusing identical
ETag/header logic.

This package owns the Server Card implementation and all stable GitHub MCP
identity/metadata, but the card is mounted and hosted only by the remote
deployment (github/github-mcp-server-remote); the standalone OSS server does
not advertise it, so self-hosted binaries never claim the dotcom remote.

supportedProtocolVersions, icons, and per-remote auth metadata are
intentionally omitted to keep the card minimal and accurate: the go-sdk does
not export negotiated protocol versions, and auth is advertised via OAuth
protected-resource-metadata discovery rather than duplicated on the card.

Refs github/copilot-mcp-core#1855, epic github/copilot-mcp-core#1853
Spec: modelcontextprotocol/experimental-ext-server-card

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4a9f522f-6942-4b77-98a4-b2d42f19625d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

pkg/http/servercard/handler.go:112

  • This has the same repeated-field issue as Accept: If-None-Match is list-valued, but Header.Get examines only its first field line. If a matching validator is supplied on a later line, this returns 200 instead of the required 304. Combine Header.Values before applying the weak comparison.
	if ifNoneMatchSatisfied(r.Header.Get("If-None-Match"), etag) {

pkg/http/servercard/handler.go:204

  • Discarding every non-q parameter is not RFC 9110 media-range matching. For example, Accept: application/mcp-server-card+json;profile=x constrains the acceptable representation to that parameter, but this parameterless response returns 200; a parameterized exact q=0 can also incorrectly override an acceptable wildcard. Parse media parameters and only treat a range as matching when its representation parameters match the emitted Content-Type, with regression tests for both cases.
// parseMediaRange splits one Accept media range into its lowercased media type
// and quality value. The quality defaults to 1.0 when no valid q parameter is
// present; only the q parameter is interpreted and other parameters are ignored.
func parseMediaRange(part string) (mediaType string, quality float64) {
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

return
}

if !acceptsCard(r.Header.Get(headers.AcceptHeader)) {
Comment on lines +49 to +53
// ServerCard is a static metadata document describing a remote MCP server,
// suitable for pre-connection discovery. It mirrors the ServerCard interface in
// modelcontextprotocol/experimental-ext-server-card. Server Cards are
// remote-only and never carry installable packages.
type ServerCard struct {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants