Skip to content

Commit 6b3c375

Browse files
feat: Add Octicon icons to MCP tools, resources, and prompts (#1603)
* Upgrade MCP Go SDK to v1.2.0-pre.1 and add Octicon icons to tools - Upgrade MCP Go SDK from v1.1.0 to v1.2.0-pre.1 for Icon support - Add Icon field to ToolsetMetadata for Octicon name assignment - Add OcticonURL() helper to generate CDN URLs for Octicon SVGs - Add Icons() method on ToolsetMetadata to generate MCP Icon objects - Apply icons automatically in RegisterFunc when tool is registered - Add icons to all 22 toolset metadata constants with appropriate Octicons - Update server.go to use new Capabilities API (fixes deprecation warnings) This demonstrates how the toolsets refactor makes adding new features simpler: icons are defined once in ToolsetMetadata and automatically applied to all tools in that toolset during registration. * Update third-party licenses for SDK upgrade * Address review feedback: enum size validation, mutation fix, tests - Replace runtime size validation with compile-time enum type (Size with SizeSM=16, SizeLG=24) - Fix RegisterFunc mutation by making shallow copy of tool before modifying Icons - Add comprehensive tests for octicons package (URL, Icons, Size constants) - Add toolsets tests for ToolsetMetadata.Icons(), RegisterFunc mutation prevention, and existing icon preservation - Improve icon choices for better visual semantics: - actions: play → workflow (more specific to GitHub Actions) - secret_protection: key → shield-lock (better represents protection) - gists: code → logo-gist (dedicated gist icon exists) * Add GitHub mark icon to server metadata Add the mark-github octicon to the server's Implementation struct so that MCP clients can display the GitHub logo for this server. The icon is provided in both 16x16 and 24x24 SVG sizes. * Fix rebase conflicts: use Registry methods and NullTranslationHelper - Remove duplicate old toolsets functions (AvailableToolsets, GetValidToolsetIDs, GetDefaultToolsetIDs) - Use Registry.AvailableToolsets() and Registry.HasToolset() instead - Replace stubTranslator with translations.NullTranslationHelper - Use new SDK Capabilities struct instead of deprecated HasTools/HasResources/HasPrompts - Add icon-related tests to registry_test.go * Use embedded data URIs for Octicon icons - Embed SVG icons using go:embed for offline use and faster loading - Convert icons to base64 data URIs at runtime - Fall back to CDN URL for non-embedded icons - Add test to verify all toolset icons are properly embedded - 44 SVG files (22 icons × 2 sizes) totaling ~27KB * Convert icons from SVG to PNG for MCP client compatibility MCP clients don't support SVG data URIs, so convert all embedded icons to PNG format using rsvg-convert. Changes: - Convert all 44 SVG icons to PNG format - Add 8 new icons: copilot, git-merge, repo-forked, star-fill - Update octicons.go to use PNG MIME type - Add script/fetch-icons for easy icon management - Update tests and toolsnaps for PNG format * Add mark-github icon for server metadata * Add light/dark theme icons for tools, resources, and prompts - Switch from size-based (16/24px) to theme-based (light/dark) icons - Use only 16x16 icons for smaller bundle size - Generate white (inverted) icons for dark theme backgrounds - Add icons to resources and prompts (auto-applied from toolset metadata) - Add 'file' icon for repository content resources - Update fetch-icons script to generate both theme variants * Use 24px icons with SVG fill modification for themes - Switch from 16px to 24px icons for better visibility - Use SVG fill attribute (#24292f for light, #ffffff for dark) instead of ImageMagick color inversion for cleaner theme variants - Remove ImageMagick dependency from fetch-icons script * Add specific icons for each repository resource type - repository_content: repo icon - repository_content_branch: git-branch icon - repository_content_commit: git-commit icon (new) - repository_content_tag: tag icon - repository_content_pr: git-pull-request icon Resources now have explicit icons set rather than relying on toolset fallback. * fix: restore Icon fields to toolset metadata and add icons to docs - Add Icon field to all ToolsetMetadata definitions (lost during rebase conflict resolution) - Update doc generator to include Octicon icons in toolsets table - Update doc generator to include icons in tool section headers - Use Primer Octicons CDN for GitHub markdown compatibility * feat: add icons to individual tools in documentation * fix: use repo-local icons with picture element for GitHub theme support - Reference icons from pkg/octicons/icons/ instead of external CDN - Use picture element with prefers-color-scheme for light/dark mode - GitHub markdown renderer will display these correctly * fix: remove redundant icons from individual tools Icons are kept on section headers and toolsets table only - having the same icon on every tool within a section was visually noisy and redundant. * Add icons to remote server toolsets documentation * Fix icon paths for docs/remote-server.md * Add remote-only toolsets with auto-generated documentation and icons guide - Add ToolsetMetadataCopilot, ToolsetMetadataCopilotSpaces, ToolsetMetadataSupportSearch - Add RemoteOnlyToolsets() function to return remote-only toolset metadata - Update doc generator to auto-generate remote-only toolsets table with icons - Create docs/toolsets-and-icons.md explaining how to add icons to toolsets - Add link to icons guide in CONTRIBUTING.md * Add icon validation tests and single source of truth for required icons - Add pkg/octicons/required_icons.txt as single source of truth for icons - Add RequiredIcons() function to read the required icons list - Update script/fetch-icons to read from required_icons.txt - Update octicons_test.go to use RequiredIcons() instead of hardcoded list - Add pkg/github/toolset_icons_test.go with: - TestAllToolsetIconsExist: validates all toolset icons are embedded - TestToolsetMetadataHasIcons: ensures all toolsets have icons set - Add 'book' icon for SupportSearch toolset - Update docs/toolsets-and-icons.md with fetch-icons and CI validation docs * fix: remove unused icon parameter from writeToolDoc - Remove unused 'icon' parameter from writeToolDoc function signature - Fix whitespace inconsistency in octicons_test.go - Fixes lint failure: unused-parameter revive error * fix: combine icon with name column in remote docs for proper table rendering - Move icon from separate column to Name column with <br> separator - Keep <picture> element for light/dark theme support - Remove empty icon column that was collapsing to zero width - Remove unused octiconSimpleImg function
1 parent bc5d08d commit 6b3c375

File tree

89 files changed

+974
-93
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+974
-93
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ These are one time installations required to be able to test your changes locall
4040
- Update snapshots and run tests: `UPDATE_TOOLSNAPS=true go test ./...`
4141
- Update readme documentation: `script/generate-docs`
4242
- If renaming a tool, add a deprecation alias (see [Tool Renaming Guide](docs/tool-renaming.md))
43+
- For toolset and icon configuration, see [Toolsets and Icons Guide](docs/toolsets-and-icons.md)
4344
6. Push to your fork and [submit a pull request][pr] targeting the `main` branch
4445
7. Pat yourself on the back and wait for your pull request to be reviewed and merged.
4546

README.md

Lines changed: 38 additions & 38 deletions
Large diffs are not rendered by default.

cmd/github-mcp-server/generate_docs.go

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,52 @@ func generateRemoteServerDocs(docsPath string) error {
101101
return err
102102
}
103103

104+
// Also generate remote-only toolsets section
105+
remoteOnlyDoc := generateRemoteOnlyToolsetsDoc()
106+
updatedContent, err = replaceSection(updatedContent, "START AUTOMATED REMOTE TOOLSETS", "END AUTOMATED REMOTE TOOLSETS", remoteOnlyDoc)
107+
if err != nil {
108+
return err
109+
}
110+
104111
return os.WriteFile(docsPath, []byte(updatedContent), 0600) //#nosec G306
105112
}
106113

114+
// octiconImg returns an img tag for an Octicon that works with GitHub's light/dark theme.
115+
// Uses picture element with prefers-color-scheme for automatic theme switching.
116+
// References icons from the repo's pkg/octicons/icons directory.
117+
// Optional pathPrefix for files in subdirectories (e.g., "../" for docs/).
118+
func octiconImg(name string, pathPrefix ...string) string {
119+
if name == "" {
120+
return ""
121+
}
122+
prefix := ""
123+
if len(pathPrefix) > 0 {
124+
prefix = pathPrefix[0]
125+
}
126+
// Use picture element with media queries for light/dark mode support
127+
// GitHub renders these correctly in markdown
128+
lightIcon := fmt.Sprintf("%spkg/octicons/icons/%s-light.png", prefix, name)
129+
darkIcon := fmt.Sprintf("%spkg/octicons/icons/%s-dark.png", prefix, name)
130+
return fmt.Sprintf(`<picture><source media="(prefers-color-scheme: dark)" srcset="%s"><source media="(prefers-color-scheme: light)" srcset="%s"><img src="%s" width="20" height="20" alt="%s"></picture>`, darkIcon, lightIcon, lightIcon, name)
131+
}
132+
107133
func generateToolsetsDoc(i *inventory.Inventory) string {
108134
var buf strings.Builder
109135

110-
// Add table header and separator
111-
buf.WriteString("| Toolset | Description |\n")
112-
buf.WriteString("| ----------------------- | ------------------------------------------------------------- |\n")
136+
// Add table header and separator (with icon column)
137+
buf.WriteString("| | Toolset | Description |\n")
138+
buf.WriteString("| --- | ----------------------- | ------------------------------------------------------------- |\n")
113139

114140
// Add the context toolset row with custom description (strongly recommended)
115-
buf.WriteString("| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |\n")
141+
// Get context toolset for its icon
142+
contextIcon := octiconImg("person")
143+
fmt.Fprintf(&buf, "| %s | `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |\n", contextIcon)
116144

117145
// AvailableToolsets() returns toolsets that have tools, sorted by ID
118146
// Exclude context (custom description above) and dynamic (internal only)
119147
for _, ts := range i.AvailableToolsets("context", "dynamic") {
120-
fmt.Fprintf(&buf, "| `%s` | %s |\n", ts.ID, ts.Description)
148+
icon := octiconImg(ts.Icon)
149+
fmt.Fprintf(&buf, "| %s | `%s` | %s |\n", icon, ts.ID, ts.Description)
121150
}
122151

123152
return strings.TrimSuffix(buf.String(), "\n")
@@ -134,6 +163,7 @@ func generateToolsDoc(r *inventory.Inventory) string {
134163
var buf strings.Builder
135164
var toolBuf strings.Builder
136165
var currentToolsetID inventory.ToolsetID
166+
var currentToolsetIcon string
137167
firstSection := true
138168

139169
writeSection := func() {
@@ -145,7 +175,11 @@ func generateToolsDoc(r *inventory.Inventory) string {
145175
}
146176
firstSection = false
147177
sectionName := formatToolsetName(string(currentToolsetID))
148-
fmt.Fprintf(&buf, "<details>\n\n<summary>%s</summary>\n\n%s\n\n</details>", sectionName, strings.TrimSuffix(toolBuf.String(), "\n\n"))
178+
icon := octiconImg(currentToolsetIcon)
179+
if icon != "" {
180+
icon += " "
181+
}
182+
fmt.Fprintf(&buf, "<details>\n\n<summary>%s%s</summary>\n\n%s\n\n</details>", icon, sectionName, strings.TrimSuffix(toolBuf.String(), "\n\n"))
149183
toolBuf.Reset()
150184
}
151185

@@ -154,6 +188,7 @@ func generateToolsDoc(r *inventory.Inventory) string {
154188
if tool.Toolset.ID != currentToolsetID {
155189
writeSection()
156190
currentToolsetID = tool.Toolset.ID
191+
currentToolsetIcon = tool.Toolset.Icon
157192
}
158193
writeToolDoc(&toolBuf, tool.Tool)
159194
toolBuf.WriteString("\n\n")
@@ -190,7 +225,7 @@ func formatToolsetName(name string) string {
190225
}
191226

192227
func writeToolDoc(buf *strings.Builder, tool mcp.Tool) {
193-
// Tool name only (using annotation name instead of verbose description)
228+
// Tool name (no icon - section header already has the toolset icon)
194229
fmt.Fprintf(buf, "- **%s** - %s\n", tool.Name, tool.Annotations.Title)
195230

196231
// Parameters
@@ -302,12 +337,13 @@ func generateRemoteToolsetsDoc() string {
302337
// Build inventory - stateless
303338
r := github.NewInventory(t).Build()
304339

305-
// Generate table header
306-
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
307-
buf.WriteString("|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n")
340+
// Generate table header (icon is combined with Name column)
341+
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
342+
buf.WriteString("| ---- | ----------- | ------- | ------------------------- | -------------- | ----------------------------------- |\n")
308343

309344
// Add "all" toolset first (special case)
310-
buf.WriteString("| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) |\n")
345+
allIcon := octiconImg("apps", "../")
346+
fmt.Fprintf(&buf, "| %s<br>all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2F%%22%%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Freadonly%%22%%7D) |\n", allIcon)
311347

312348
// AvailableToolsets() returns toolsets that have tools, sorted by ID
313349
// Exclude context (handled separately) and dynamic (internal only)
@@ -329,19 +365,61 @@ func generateRemoteToolsetsDoc() string {
329365
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
330366
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)
331367

332-
fmt.Fprintf(&buf, "| %-14s | %-48s | %-53s | %-218s | %-110s | %-288s |\n",
368+
icon := octiconImg(ts.Icon, "../")
369+
fmt.Fprintf(&buf, "| %s<br>%s | %s | %s | %s | [read-only](%s) | %s |\n",
370+
icon,
333371
formattedName,
334372
ts.Description,
335373
apiURL,
336374
installLink,
337-
fmt.Sprintf("[read-only](%s)", readonlyURL),
375+
readonlyURL,
338376
readonlyInstallLink,
339377
)
340378
}
341379

342380
return strings.TrimSuffix(buf.String(), "\n")
343381
}
344382

383+
func generateRemoteOnlyToolsetsDoc() string {
384+
var buf strings.Builder
385+
386+
// Generate table header (icon is combined with Name column)
387+
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
388+
buf.WriteString("| ---- | ----------- | ------- | ------------------------- | -------------- | ----------------------------------- |\n")
389+
390+
// Use RemoteOnlyToolsets from github package
391+
for _, ts := range github.RemoteOnlyToolsets() {
392+
idStr := string(ts.ID)
393+
394+
formattedName := formatToolsetName(idStr)
395+
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", idStr)
396+
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", idStr)
397+
398+
// Create install config JSON (URL encoded)
399+
installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
400+
readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
401+
402+
// Fix URL encoding to use %20 instead of + for spaces
403+
installConfig = strings.ReplaceAll(installConfig, "+", "%20")
404+
readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
405+
406+
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
407+
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)
408+
409+
icon := octiconImg(ts.Icon, "../")
410+
fmt.Fprintf(&buf, "| %s<br>%s | %s | %s | %s | [read-only](%s) | %s |\n",
411+
icon,
412+
formattedName,
413+
ts.Description,
414+
apiURL,
415+
installLink,
416+
readonlyURL,
417+
readonlyInstallLink,
418+
)
419+
}
420+
421+
return strings.TrimSuffix(buf.String(), "\n")
422+
}
345423
func generateDeprecatedAliasesDocs(docsPath string) error {
346424
// Read the current file
347425
content, err := os.ReadFile(docsPath) //#nosec G304

0 commit comments

Comments
 (0)