Add MCP Server Card (SEP-2127) types + handler - #2768
Open
SamMorrowDrums wants to merge 1 commit into
Open
Conversation
SamMorrowDrums
force-pushed
the
sammorrowdrums-server-card-handler
branch
2 times, most recently
from
August 26, 2026 10:34
eaa898f to
06a878c
Compare
Contributor
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
Contributor
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
SamMorrowDrums
force-pushed
the
sammorrowdrums-server-card-handler
branch
3 times, most recently
from
August 26, 2026 11:01
e33dc64 to
5a4ca43
Compare
Contributor
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
SamMorrowDrums
force-pushed
the
sammorrowdrums-server-card-handler
branch
from
August 28, 2026 10:41
5a4ca43 to
0abcada
Compare
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
force-pushed
the
sammorrowdrums-server-card-handler
branch
from
August 28, 2026 10:46
0abcada to
ad12429
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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