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 thread pkg/http/servercard/card.go
Comment thread pkg/http/servercard/card.go
Comment thread pkg/http/servercard/handler.go Outdated
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 3 times, most recently from e33dc64 to 5a4ca43 Compare August 26, 2026 11:01
@SamMorrowDrums
SamMorrowDrums requested a balanced review from Copilot August 26, 2026 11:04

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

Comment thread pkg/http/servercard/handler.go Outdated
Comment thread pkg/http/servercard/card.go
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-server-card-handler branch from 5a4ca43 to 0abcada Compare August 28, 2026 10:41
Define the GitHub MCP Server's Server Card (SEP-2127, in review) and a
public no-auth HTTP handler that serves it at the canonical /server-card
backend path. OSS owns all stable identity/metadata and serving behavior;
the remote server only supplies an environment-specific remote URL.

Negotiate list-valued Accept and If-None-Match across repeated header
field-lines so multi-line requests are handled correctly.

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
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-server-card-handler branch from 0abcada to ad12429 Compare August 28, 2026 10:46
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