Skip to content

Commit fa7c948

Browse files
Add MCP Server Card (SEP-2127) types + handler
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 Accept and If-None-Match as RFC 9110 list values across repeated header field-lines. Accept honors quoted commas, media-range parameters, and q-values (q=0 rejects); entity-tag lists split on quoted commas without backslash escaping, since an opaque-tag treats a backslash literally. Vary lists Accept and X-Forwarded-Host and is appended rather than set: it composes with values added by deployment middleware and, since the response is publicly cacheable and multi-tenant deployments derive the remote URL from the trusted X-Forwarded-Host, it keeps a shared cache from serving one tenant's card to another. 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
1 parent 12d16ed commit fa7c948

4 files changed

Lines changed: 800 additions & 0 deletions

File tree

pkg/http/servercard/card.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Package servercard provides the GitHub MCP Server's MCP Server Card
2+
// (SEP-2127) types and a public, no-auth HTTP handler that serves it.
3+
//
4+
// A Server Card is a static metadata document that describes a remote MCP
5+
// server — its identity, repository, and HTTP transport — so clients can
6+
// discover and connect to it before the protocol handshake. It is remote-only
7+
// and deliberately does NOT enumerate primitives (tools, resources, prompts)
8+
// or installable packages; those remain in the MCP Registry document
9+
// (server.json) and runtime listing.
10+
//
11+
// See:
12+
// - https://github.com/modelcontextprotocol/experimental-ext-server-card
13+
// - https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127
14+
package servercard
15+
16+
import "net/http"
17+
18+
const (
19+
// SchemaURL is the v1 Server Card JSON Schema URI that emitted cards
20+
// conform to. The schema is versioned by its `vN` path segment.
21+
SchemaURL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json"
22+
23+
// MediaType is the media type used to serve and request a Server Card.
24+
MediaType = "application/mcp-server-card+json"
25+
26+
// Path is the suffix, relative to a server's streamable-HTTP URL, at which
27+
// MCP reserves the recommended Server Card location. A server hosted at
28+
// `https://host/mcp` therefore serves its card at `https://host/mcp/server-card`.
29+
Path = "/server-card"
30+
31+
// DefaultRemoteURL is the streamable-HTTP endpoint of the hosted GitHub MCP
32+
// Server on github.com. The remote repository overrides this per environment.
33+
DefaultRemoteURL = "https://api.githubcopilot.com/mcp/"
34+
)
35+
36+
// Identity fields reused from the MCP Registry document (server.json) so the
37+
// Server Card and the registry entry describe the same server.
38+
const (
39+
serverName = "io.github.github/github-mcp-server"
40+
serverTitle = "GitHub"
41+
serverDescription = "Connect AI assistants to GitHub - manage repos, issues, PRs, and workflows through natural language."
42+
repositoryURL = "https://github.com/github/github-mcp-server"
43+
repositorySource = "github"
44+
// repositoryID is the github.com repository ID for github/github-mcp-server.
45+
// It is stable across renames but changes if the repository is recreated.
46+
repositoryID = "942771284"
47+
)
48+
49+
// ServerCard is a static metadata document describing a remote MCP server,
50+
// suitable for pre-connection discovery. It mirrors the ServerCard interface in
51+
// modelcontextprotocol/experimental-ext-server-card. Server Cards are
52+
// remote-only and never carry installable packages.
53+
type ServerCard struct {
54+
// Schema is the Server Card JSON Schema URI this document conforms to.
55+
Schema string `json:"$schema"`
56+
// Name is the server name in reverse-DNS format with exactly one slash.
57+
Name string `json:"name"`
58+
// Version is the server version, equivalent to Implementation.version.
59+
Version string `json:"version"`
60+
// Description is a short, human-readable explanation of server functionality.
61+
Description string `json:"description"`
62+
// Title is an optional human-readable display name.
63+
Title string `json:"title,omitempty"`
64+
// WebsiteURL optionally links to the server's homepage or documentation.
65+
WebsiteURL string `json:"websiteUrl,omitempty"`
66+
// Repository optionally describes the server's source code for inspection.
67+
Repository *Repository `json:"repository,omitempty"`
68+
// Remotes lists the HTTP-based endpoints for connecting to the server.
69+
Remotes []Remote `json:"remotes,omitempty"`
70+
}
71+
72+
// Repository describes the MCP server's source code location.
73+
type Repository struct {
74+
// URL is the repository URL for browsing source and cloning.
75+
URL string `json:"url"`
76+
// Source is the hosting service identifier (e.g. "github").
77+
Source string `json:"source"`
78+
// ID is the optional repository identifier owned by the hosting service.
79+
ID string `json:"id,omitempty"`
80+
}
81+
82+
// Remote describes a remote (HTTP-based) MCP server endpoint. Authentication is
83+
// intentionally not described here: the hosted server advertises its auth
84+
// requirements via OAuth protected-resource-metadata discovery, so duplicating
85+
// them on the card would risk drift and cannot capture every accepted mode.
86+
type Remote struct {
87+
// Type is the transport type ("streamable-http" or "sse").
88+
Type string `json:"type"`
89+
// URL is the endpoint URL.
90+
URL string `json:"url"`
91+
}
92+
93+
// Config controls how the GitHub MCP Server card is built and served.
94+
type Config struct {
95+
// Version is advertised as the card's version and SHOULD match the
96+
// runtime serverInfo version. When empty, "0.0.0-dev" is used.
97+
Version string
98+
99+
// RemoteURL is the absolute streamable-HTTP endpoint advertised in the
100+
// card's single remote. When empty, DefaultRemoteURL is used. The remote
101+
// repository supplies a per-environment URL here.
102+
RemoteURL string
103+
104+
// RemoteURLFunc, when set, derives the streamable-HTTP remote URL from the
105+
// incoming request, taking precedence over RemoteURL whenever it returns a
106+
// non-empty value. This supports multi-tenant deployments (e.g. proxima)
107+
// where the absolute URL varies per request (e.g. from X-Forwarded-Host).
108+
//
109+
// It is consumed by the Handler when serving a card; NewServerCard ignores
110+
// it, since the card constructor is not request-aware.
111+
RemoteURLFunc func(*http.Request) string
112+
}
113+
114+
// NewServerCard builds the GitHub MCP Server's Server Card from cfg.
115+
func NewServerCard(cfg Config) *ServerCard {
116+
version := cfg.Version
117+
if version == "" {
118+
version = "0.0.0-dev"
119+
}
120+
121+
remoteURL := cfg.RemoteURL
122+
if remoteURL == "" {
123+
remoteURL = DefaultRemoteURL
124+
}
125+
126+
// supportedProtocolVersions is intentionally omitted: the go-sdk does not
127+
// export the versions it negotiates, so advertising a hand-maintained list
128+
// here would risk drifting from what the server actually serves.
129+
return &ServerCard{
130+
Schema: SchemaURL,
131+
Name: serverName,
132+
Version: version,
133+
Description: serverDescription,
134+
Title: serverTitle,
135+
WebsiteURL: repositoryURL,
136+
Repository: &Repository{
137+
URL: repositoryURL,
138+
Source: repositorySource,
139+
ID: repositoryID,
140+
},
141+
Remotes: []Remote{
142+
{Type: "streamable-http", URL: remoteURL},
143+
},
144+
}
145+
}

pkg/http/servercard/card_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package servercard
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// assertCardContract checks the required Server Card fields defined by the
13+
// experimental-ext-server-card v1 schema, plus the remote-only invariant. The
14+
// card is small and stable, so asserting its required fields is a focused
15+
// stand-in for vendoring the upstream JSON schema.
16+
func assertCardContract(t *testing.T, card *ServerCard) {
17+
t.Helper()
18+
19+
raw, err := json.Marshal(card)
20+
require.NoError(t, err)
21+
22+
var fields map[string]json.RawMessage
23+
require.NoError(t, json.Unmarshal(raw, &fields))
24+
25+
// Required by the schema: $schema, name, version, description.
26+
assert.Equal(t, SchemaURL, card.Schema)
27+
assert.NotEmpty(t, card.Name)
28+
assert.NotEmpty(t, card.Version)
29+
require.NotEmpty(t, card.Description)
30+
assert.LessOrEqual(t, len(card.Description), 100, "description must respect the schema maxLength")
31+
for _, key := range []string{"$schema", "name", "version", "description"} {
32+
assert.Contains(t, fields, key, "required field %q must be serialized", key)
33+
}
34+
35+
// Remote-only: a Server Card never enumerates installable packages — those
36+
// stay in the registry server.json.
37+
assert.NotContains(t, fields, "packages", "Server Card must be remote-only and omit packages")
38+
require.Len(t, card.Remotes, 1)
39+
assert.Equal(t, "streamable-http", card.Remotes[0].Type)
40+
}
41+
42+
func TestNewServerCard(t *testing.T) {
43+
t.Parallel()
44+
45+
tests := []struct {
46+
name string
47+
cfg Config
48+
expectedVersion string
49+
expectedRemoteURL string
50+
}{
51+
{
52+
name: "defaults",
53+
cfg: Config{},
54+
expectedVersion: "0.0.0-dev",
55+
expectedRemoteURL: DefaultRemoteURL,
56+
},
57+
{
58+
name: "explicit version",
59+
cfg: Config{Version: "1.2.3"},
60+
expectedVersion: "1.2.3",
61+
expectedRemoteURL: DefaultRemoteURL,
62+
},
63+
{
64+
name: "per-environment remote URL",
65+
cfg: Config{Version: "1.2.3", RemoteURL: "https://api.example.test/mcp/"},
66+
expectedVersion: "1.2.3",
67+
expectedRemoteURL: "https://api.example.test/mcp/",
68+
},
69+
}
70+
71+
for _, tc := range tests {
72+
t.Run(tc.name, func(t *testing.T) {
73+
t.Parallel()
74+
75+
card := NewServerCard(tc.cfg)
76+
77+
// Identity is reused from the registry document (server.json): it is
78+
// the stable Server Card / registry server name. The AI Catalog
79+
// identifier is assigned independently and is not derived from it.
80+
assert.Equal(t, "io.github.github/github-mcp-server", card.Name)
81+
assert.Equal(t, "GitHub", card.Title)
82+
assert.True(t, strings.HasPrefix(card.Description, "Connect AI assistants to GitHub"))
83+
assert.Equal(t, tc.expectedVersion, card.Version)
84+
assert.Equal(t, "https://github.com/github/github-mcp-server", card.WebsiteURL)
85+
86+
require.NotNil(t, card.Repository)
87+
assert.Equal(t, "https://github.com/github/github-mcp-server", card.Repository.URL)
88+
assert.Equal(t, "github", card.Repository.Source)
89+
assert.Equal(t, "942771284", card.Repository.ID)
90+
91+
assert.Equal(t, tc.expectedRemoteURL, card.Remotes[0].URL)
92+
93+
assertCardContract(t, card)
94+
})
95+
}
96+
}

0 commit comments

Comments
 (0)