Skip to content

Commit 172dc95

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, honoring quoted commas, media-range parameters, and q-values (q=0 rejects) so browser and multi-line requests are handled correctly. Vary: Accept is appended rather than set so it composes with any values added by deployment middleware. 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 172dc95

4 files changed

Lines changed: 942 additions & 0 deletions

File tree

pkg/http/servercard/card.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 list of versions it negotiates, so we cannot advertise it
128+
// accurately from the runtime. Omitting it is preferable to publishing a
129+
// hand-maintained list that could drift from what the server actually
130+
// serves.
131+
return &ServerCard{
132+
Schema: SchemaURL,
133+
Name: serverName,
134+
Version: version,
135+
Description: serverDescription,
136+
Title: serverTitle,
137+
WebsiteURL: repositoryURL,
138+
Repository: &Repository{
139+
URL: repositoryURL,
140+
Source: repositorySource,
141+
ID: repositoryID,
142+
},
143+
Remotes: []Remote{
144+
{Type: "streamable-http", URL: remoteURL},
145+
},
146+
}
147+
}

pkg/http/servercard/card_test.go

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

0 commit comments

Comments
 (0)