Skip to content

Commit 9143074

Browse files
Enrich Server Card with icons and supported protocol versions
Populate the optional forward fields the v1 Server Card schema already defines so the card is complete for discovery: - icons: the GitHub mark in light and dark themes, reusing the embedded Octicons as self-contained data URIs so the card has no external image dependency. The icon order is fixed to keep the serialized card — and thus its ETag — deterministic. - supportedProtocolVersions on the streamable-http remote, defaulted from DefaultProtocolVersions (mirroring the bundled go-sdk's supported versions, which are unexported) and overridable via Config. The canonical card name io.github.github/github-mcp-server is unchanged, preserving the downstream AI Catalog identity derived from it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0925dd2 commit 9143074

2 files changed

Lines changed: 123 additions & 17 deletions

File tree

pkg/http/servercard/card.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
// - https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127
1414
package servercard
1515

16-
import "net/http"
16+
import (
17+
"net/http"
18+
19+
"github.com/github/github-mcp-server/pkg/octicons"
20+
)
1721

1822
const (
1923
// SchemaURL is the v1 Server Card JSON Schema URI that emitted cards
@@ -31,8 +35,26 @@ const (
3135
// DefaultRemoteURL is the streamable-HTTP endpoint of the hosted GitHub MCP
3236
// Server on github.com. The remote repository overrides this per environment.
3337
DefaultRemoteURL = "https://api.githubcopilot.com/mcp/"
38+
39+
// iconName is the Octicon used as the server's icon (the GitHub mark).
40+
iconName = "mark-github"
41+
42+
// iconSize is the pixel dimension of the embedded Octicon PNGs (square).
43+
iconSize = "24x24"
3444
)
3545

46+
// DefaultProtocolVersions lists the MCP protocol versions advertised on the
47+
// card's remote when Config.ProtocolVersions is not set. It mirrors the
48+
// versions supported by the bundled go-sdk (see modelcontextprotocol/go-sdk
49+
// mcp.supportedProtocolVersions, which is unexported) and is ordered newest
50+
// first. Keep it in sync when the go-sdk dependency in go.mod is upgraded.
51+
var DefaultProtocolVersions = []string{
52+
"2025-11-25",
53+
"2025-06-18",
54+
"2025-03-26",
55+
"2024-11-05",
56+
}
57+
3658
// Identity fields reused from the MCP Registry document (server.json) so the
3759
// Server Card and the registry entry describe the same server.
3860
const (
@@ -161,6 +183,10 @@ type Config struct {
161183
// It is consumed by the Handler when serving a card; NewServerCard ignores
162184
// it, since the card constructor is not request-aware.
163185
RemoteURLFunc func(*http.Request) string
186+
187+
// ProtocolVersions overrides the MCP protocol versions advertised on the
188+
// card's remote. When empty, DefaultProtocolVersions is used.
189+
ProtocolVersions []string
164190
}
165191

166192
// NewServerCard builds the GitHub MCP Server's Server Card from cfg.
@@ -175,13 +201,19 @@ func NewServerCard(cfg Config) *ServerCard {
175201
remoteURL = DefaultRemoteURL
176202
}
177203

204+
protocolVersions := cfg.ProtocolVersions
205+
if protocolVersions == nil {
206+
protocolVersions = DefaultProtocolVersions
207+
}
208+
178209
return &ServerCard{
179210
Schema: SchemaURL,
180211
Name: serverName,
181212
Version: version,
182213
Description: serverDescription,
183214
Title: serverTitle,
184215
WebsiteURL: repositoryURL,
216+
Icons: githubIcons(),
185217
Repository: &Repository{
186218
URL: repositoryURL,
187219
Source: repositorySource,
@@ -201,7 +233,36 @@ func NewServerCard(cfg Config) *ServerCard {
201233
Name: "Authorization",
202234
},
203235
},
236+
SupportedProtocolVersions: protocolVersions,
204237
},
205238
},
206239
}
207240
}
241+
242+
// githubIcons returns the light- and dark-theme GitHub mark icons as
243+
// self-contained data URIs, reusing the embedded Octicons so the card has no
244+
// external image dependency. The order is fixed so the serialized card — and
245+
// therefore its ETag — is deterministic. It returns nil if the icons are
246+
// unavailable.
247+
func githubIcons() []Icon {
248+
themes := []struct {
249+
octicon octicons.Theme
250+
card string
251+
}{
252+
{octicons.ThemeLight, "light"},
253+
{octicons.ThemeDark, "dark"},
254+
}
255+
256+
var icons []Icon
257+
for _, t := range themes {
258+
if src := octicons.DataURI(iconName, t.octicon); src != "" {
259+
icons = append(icons, Icon{
260+
Src: src,
261+
MimeType: "image/png",
262+
Sizes: []string{iconSize},
263+
Theme: t.card,
264+
})
265+
}
266+
}
267+
return icons
268+
}

pkg/http/servercard/card_test.go

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,28 +53,39 @@ func TestNewServerCard(t *testing.T) {
5353
resolved := resolvedCardSchema(t)
5454

5555
tests := []struct {
56-
name string
57-
cfg Config
58-
expectedVersion string
59-
expectedRemoteURL string
56+
name string
57+
cfg Config
58+
expectedVersion string
59+
expectedRemoteURL string
60+
expectedProtocolVersions []string
6061
}{
6162
{
62-
name: "defaults",
63-
cfg: Config{},
64-
expectedVersion: "0.0.0-dev",
65-
expectedRemoteURL: DefaultRemoteURL,
63+
name: "defaults",
64+
cfg: Config{},
65+
expectedVersion: "0.0.0-dev",
66+
expectedRemoteURL: DefaultRemoteURL,
67+
expectedProtocolVersions: DefaultProtocolVersions,
6668
},
6769
{
68-
name: "explicit version",
69-
cfg: Config{Version: "1.2.3"},
70-
expectedVersion: "1.2.3",
71-
expectedRemoteURL: DefaultRemoteURL,
70+
name: "explicit version",
71+
cfg: Config{Version: "1.2.3"},
72+
expectedVersion: "1.2.3",
73+
expectedRemoteURL: DefaultRemoteURL,
74+
expectedProtocolVersions: DefaultProtocolVersions,
7275
},
7376
{
74-
name: "per-environment remote URL",
75-
cfg: Config{Version: "1.2.3", RemoteURL: "https://api.example.test/mcp/"},
76-
expectedVersion: "1.2.3",
77-
expectedRemoteURL: "https://api.example.test/mcp/",
77+
name: "per-environment remote URL",
78+
cfg: Config{Version: "1.2.3", RemoteURL: "https://api.example.test/mcp/"},
79+
expectedVersion: "1.2.3",
80+
expectedRemoteURL: "https://api.example.test/mcp/",
81+
expectedProtocolVersions: DefaultProtocolVersions,
82+
},
83+
{
84+
name: "explicit protocol versions",
85+
cfg: Config{ProtocolVersions: []string{"2025-06-18"}},
86+
expectedVersion: "0.0.0-dev",
87+
expectedRemoteURL: DefaultRemoteURL,
88+
expectedProtocolVersions: []string{"2025-06-18"},
7889
},
7990
}
8091

@@ -88,6 +99,7 @@ func TestNewServerCard(t *testing.T) {
8899
assert.Equal(t, "io.github.github/github-mcp-server", card.Name)
89100
assert.Equal(t, "GitHub", card.Title)
90101
assert.Equal(t, tc.expectedVersion, card.Version)
102+
assert.Equal(t, "https://github.com/github/github-mcp-server", card.WebsiteURL)
91103
assert.LessOrEqual(t, len(card.Description), 100, "description must respect the schema maxLength")
92104

93105
require.NotNil(t, card.Repository)
@@ -101,12 +113,45 @@ func TestNewServerCard(t *testing.T) {
101113
require.Len(t, card.Remotes[0].Headers, 1)
102114
assert.Equal(t, "Authorization", card.Remotes[0].Headers[0].Name)
103115
assert.True(t, card.Remotes[0].Headers[0].IsSecret)
116+
assert.Equal(t, tc.expectedProtocolVersions, card.Remotes[0].SupportedProtocolVersions)
104117

105118
assertSchemaValid(t, resolved, card)
106119
})
107120
}
108121
}
109122

123+
// TestServerCardIcons verifies the card advertises the self-contained GitHub
124+
// mark icons in both themes.
125+
func TestServerCardIcons(t *testing.T) {
126+
t.Parallel()
127+
128+
card := NewServerCard(Config{})
129+
130+
require.Len(t, card.Icons, 2)
131+
themes := make(map[string]Icon, len(card.Icons))
132+
for _, icon := range card.Icons {
133+
assert.True(t, strings.HasPrefix(icon.Src, "data:image/png;base64,"), "icon must be a self-contained data URI")
134+
assert.Equal(t, "image/png", icon.MimeType)
135+
assert.Equal(t, []string{"24x24"}, icon.Sizes)
136+
themes[icon.Theme] = icon
137+
}
138+
assert.Contains(t, themes, "light")
139+
assert.Contains(t, themes, "dark")
140+
}
141+
142+
// TestServerCardIsDeterministic guards the ETag contract: identical Config must
143+
// always marshal to identical bytes, so unordered sources (e.g. icons) cannot
144+
// destabilize the response hash.
145+
func TestServerCardIsDeterministic(t *testing.T) {
146+
t.Parallel()
147+
148+
first, err := json.Marshal(NewServerCard(Config{Version: "1.2.3"}))
149+
require.NoError(t, err)
150+
second, err := json.Marshal(NewServerCard(Config{Version: "1.2.3"}))
151+
require.NoError(t, err)
152+
assert.Equal(t, first, second)
153+
}
154+
110155
// TestServerCardIsRemoteOnly guards the SEP-2127 requirement that a Server Card
111156
// never enumerates installable packages — those stay in the registry server.json.
112157
func TestServerCardIsRemoteOnly(t *testing.T) {

0 commit comments

Comments
 (0)