Add MCP Server Card (SEP-2127) types + handler - #2768
Conversation
eaa898f to
06a878c
Compare
There was a problem hiding this comment.
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-cardroute.
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
| // 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). |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Review details
Suppressed comments (2)
pkg/http/servercard/handler.go:181
- The
Acceptparser discards every parameter, soAccept: application/mcp-server-card+json;q=0(andapplication/*;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 forMediaTypeis 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
Acceptand the 200 response is publicly cacheable, it needsVary: 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
273b365 to
e33dc64
Compare
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
e33dc64 to
5a4ca43
Compare
There was a problem hiding this comment.
Review details
Suppressed comments (2)
pkg/http/servercard/handler.go:112
- This has the same repeated-field issue as
Accept:If-None-Matchis list-valued, butHeader.Getexamines only its first field line. If a matching validator is supplied on a later line, this returns 200 instead of the required 304. CombineHeader.Valuesbefore applying the weak comparison.
if ifNoneMatchSatisfied(r.Header.Get("If-None-Match"), etag) {
pkg/http/servercard/handler.go:204
- Discarding every non-
qparameter is not RFC 9110 media-range matching. For example,Accept: application/mcp-server-card+json;profile=xconstrains the acceptable representation to that parameter, but this parameterless response returns 200; a parameterized exactq=0can also incorrectly override an acceptable wildcard. Parse media parameters and only treat a range as matching when its representation parameters match the emittedContent-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)) { |
| // 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 { |
What & why
Makes the GitHub MCP Server discoverable via an MCP Server Card. Adds a new OSS-owned package
pkg/http/servercardwith:modelcontextprotocol/experimental-ext-server-card(schema.json+docs/discovery.md).NewServerCard) for the GitHub MCP Server's card, reusing the identity fields from the registryserver.jsonso both documents describe the same server.http.Handlerthat serves the card at the reserved/server-cardlocation (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.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
nameis locked toio.github.github/github-mcp-server, matching the registryserver.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)
application/mcp-server-card+jsonwith RFC 9110Acceptnegotiation, including quality values — an explicitq=0on the most specific matching media range rejects with406 Not Acceptable.Access-Control-Allow-Origin: *Access-Control-Allow-Methods: GETAccess-Control-Allow-Headers: Content-Type, If-None-MatchAccess-Control-Expose-Headers: ETagCache-Control: public, max-age=3600.If-None-Match→304 Not Modified(empty body). Weak comparison per RFC 9110. See upstream ETag proposal experimental-ext-server-card#33.RegisterRoutesmounts the card at the single reserved/server-cardpath. 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)
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.title/description/repository/websiteUrlare sufficient for discovery; icon data-URIs added code without contract value.type+urlon a remote.Validation
go build ./...,go test -race ./...(full suite),./script/lint(0 issues) — all green.main(==v1.11.0) and squashed into a single commit.Refs: github/copilot-mcp-core#1855 · epic github/copilot-mcp-core#1853