chore: import upstream snapshot with attribution
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 397 B |
|
After Width: | Height: | Size: 391 B |
|
After Width: | Height: | Size: 590 B |
|
After Width: | Height: | Size: 429 B |
|
After Width: | Height: | Size: 642 B |
|
After Width: | Height: | Size: 318 B |
|
After Width: | Height: | Size: 395 B |
|
After Width: | Height: | Size: 590 B |
|
After Width: | Height: | Size: 918 B |
|
After Width: | Height: | Size: 381 B |
|
After Width: | Height: | Size: 511 B |
|
After Width: | Height: | Size: 561 B |
|
After Width: | Height: | Size: 941 B |
|
After Width: | Height: | Size: 359 B |
|
After Width: | Height: | Size: 488 B |
|
After Width: | Height: | Size: 595 B |
|
After Width: | Height: | Size: 806 B |
|
After Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 417 B |
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 476 B |
|
After Width: | Height: | Size: 728 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 448 B |
|
After Width: | Height: | Size: 471 B |
|
After Width: | Height: | Size: 707 B |
|
After Width: | Height: | Size: 490 B |
|
After Width: | Height: | Size: 780 B |
|
After Width: | Height: | Size: 527 B |
|
After Width: | Height: | Size: 804 B |
|
After Width: | Height: | Size: 747 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 886 B |
|
After Width: | Height: | Size: 377 B |
|
After Width: | Height: | Size: 499 B |
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 933 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 660 B |
|
After Width: | Height: | Size: 281 B |
|
After Width: | Height: | Size: 338 B |
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 771 B |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 501 B |
|
After Width: | Height: | Size: 766 B |
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 814 B |
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 375 B |
|
After Width: | Height: | Size: 614 B |
|
After Width: | Height: | Size: 785 B |
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 461 B |
|
After Width: | Height: | Size: 521 B |
|
After Width: | Height: | Size: 809 B |
|
After Width: | Height: | Size: 375 B |
|
After Width: | Height: | Size: 500 B |
@@ -0,0 +1,84 @@
|
||||
// Package octicons provides helpers for working with GitHub Octicon icons.
|
||||
// See https://primer.style/foundations/icons for available icons.
|
||||
package octicons
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
//go:embed icons/*.png
|
||||
var iconsFS embed.FS
|
||||
|
||||
//go:embed required_icons.txt
|
||||
var requiredIconsTxt string
|
||||
|
||||
// RequiredIcons returns the list of icon names from required_icons.txt.
|
||||
// This is the single source of truth for which icons should be embedded.
|
||||
func RequiredIcons() []string {
|
||||
var icons []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(requiredIconsTxt))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
// Skip empty lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
icons = append(icons, line)
|
||||
}
|
||||
return icons
|
||||
}
|
||||
|
||||
// Theme represents the color theme of an icon.
|
||||
type Theme string
|
||||
|
||||
const (
|
||||
// ThemeLight is for light backgrounds (dark/black icons).
|
||||
ThemeLight Theme = "light"
|
||||
// ThemeDark is for dark backgrounds (light/white icons).
|
||||
ThemeDark Theme = "dark"
|
||||
)
|
||||
|
||||
// DataURI returns a data URI for the embedded Octicon PNG.
|
||||
// The theme parameter specifies which variant to use:
|
||||
// - ThemeLight: dark icons for light backgrounds
|
||||
// - ThemeDark: light icons for dark backgrounds
|
||||
// If the icon is not found in the embedded filesystem, it returns an empty string.
|
||||
func DataURI(name string, theme Theme) string {
|
||||
filename := fmt.Sprintf("icons/%s-%s.png", name, theme)
|
||||
data, err := iconsFS.ReadFile(filename)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
// Icons returns MCP Icon objects for the given octicon name in light and dark themes.
|
||||
// Icons are embedded as 24x24 PNG data URIs for offline use and faster loading.
|
||||
// The name should be the base octicon name without size suffix (e.g., "repo" not "repo-16").
|
||||
// See https://primer.style/foundations/icons for available icons.
|
||||
//
|
||||
// Note: The Sizes field is omitted for backward compatibility with older MCP clients
|
||||
// that expect it to be a string rather than an array per the 2025-11-25 MCP spec.
|
||||
func Icons(name string) []mcp.Icon {
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
return []mcp.Icon{
|
||||
{
|
||||
Source: DataURI(name, ThemeLight),
|
||||
MIMEType: "image/png",
|
||||
Theme: mcp.IconThemeLight,
|
||||
},
|
||||
{
|
||||
Source: DataURI(name, ThemeDark),
|
||||
MIMEType: "image/png",
|
||||
Theme: mcp.IconThemeDark,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package octicons
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDataURI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
icon string
|
||||
theme Theme
|
||||
wantDataURI bool
|
||||
wantEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "light theme icon returns data URI",
|
||||
icon: "repo",
|
||||
theme: ThemeLight,
|
||||
wantDataURI: true,
|
||||
wantEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "dark theme icon returns data URI",
|
||||
icon: "repo",
|
||||
theme: ThemeDark,
|
||||
wantDataURI: true,
|
||||
wantEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "non-embedded icon returns empty string",
|
||||
icon: "nonexistent-icon",
|
||||
theme: ThemeLight,
|
||||
wantDataURI: false,
|
||||
wantEmpty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := DataURI(tc.icon, tc.theme)
|
||||
if tc.wantDataURI {
|
||||
assert.True(t, strings.HasPrefix(result, "data:image/png;base64,"), "expected data URI prefix")
|
||||
assert.NotContains(t, result, "https://")
|
||||
}
|
||||
if tc.wantEmpty {
|
||||
assert.Empty(t, result, "expected empty string for non-embedded icon")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIcons(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
icon string
|
||||
wantNil bool
|
||||
wantCount int
|
||||
}{
|
||||
{
|
||||
name: "valid embedded icon returns light and dark variants",
|
||||
icon: "repo",
|
||||
wantNil: false,
|
||||
wantCount: 2,
|
||||
},
|
||||
{
|
||||
name: "empty name returns nil",
|
||||
icon: "",
|
||||
wantNil: true,
|
||||
wantCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := Icons(tc.icon)
|
||||
if tc.wantNil {
|
||||
assert.Nil(t, result)
|
||||
return
|
||||
}
|
||||
assert.NotNil(t, result)
|
||||
assert.Len(t, result, tc.wantCount)
|
||||
|
||||
// Verify first icon is light theme
|
||||
assert.Equal(t, DataURI(tc.icon, ThemeLight), result[0].Source)
|
||||
assert.Equal(t, "image/png", result[0].MIMEType)
|
||||
assert.Empty(t, result[0].Sizes) // Sizes field omitted for backward compatibility
|
||||
assert.Equal(t, mcp.IconThemeLight, result[0].Theme)
|
||||
|
||||
// Verify second icon is dark theme
|
||||
assert.Equal(t, DataURI(tc.icon, ThemeDark), result[1].Source)
|
||||
assert.Equal(t, "image/png", result[1].MIMEType)
|
||||
assert.Empty(t, result[1].Sizes) // Sizes field omitted for backward compatibility
|
||||
assert.Equal(t, mcp.IconThemeDark, result[1].Theme)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemeConstants(t *testing.T) {
|
||||
assert.Equal(t, Theme("light"), ThemeLight)
|
||||
assert.Equal(t, Theme("dark"), ThemeDark)
|
||||
}
|
||||
|
||||
func TestEmbeddedIconsExist(t *testing.T) {
|
||||
// Test that all required icons from required_icons.txt are properly embedded
|
||||
// This is the single source of truth for which icons should be available
|
||||
expectedIcons := RequiredIcons()
|
||||
for _, icon := range expectedIcons {
|
||||
t.Run(icon, func(t *testing.T) {
|
||||
lightURI := DataURI(icon, ThemeLight)
|
||||
darkURI := DataURI(icon, ThemeDark)
|
||||
assert.True(t, strings.HasPrefix(lightURI, "data:image/png;base64,"), "light theme icon %s should be embedded", icon)
|
||||
assert.True(t, strings.HasPrefix(darkURI, "data:image/png;base64,"), "dark theme icon %s should be embedded", icon)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Required Octicons for the GitHub MCP Server
|
||||
# This file is the source of truth for icon requirements.
|
||||
# Used by:
|
||||
# - script/fetch-icons (to download icons)
|
||||
# - pkg/octicons/octicons_test.go (to validate icons are embedded)
|
||||
# - pkg/github/toolset_icons_test.go (to validate toolset icons exist)
|
||||
#
|
||||
# Add new icons here when:
|
||||
# - Adding a new toolset with an icon
|
||||
# - Adding a new tool that needs a custom icon
|
||||
#
|
||||
# Format: one icon name per line (without -24.svg suffix)
|
||||
# Lines starting with # are comments
|
||||
# Empty lines are ignored
|
||||
|
||||
apps
|
||||
beaker
|
||||
bell
|
||||
book
|
||||
check-circle
|
||||
codescan
|
||||
code-square
|
||||
comment-discussion
|
||||
copilot
|
||||
dependabot
|
||||
file
|
||||
git-branch
|
||||
git-commit
|
||||
git-merge
|
||||
git-pull-request
|
||||
issue-opened
|
||||
logo-gist
|
||||
mark-github
|
||||
organization
|
||||
people
|
||||
person
|
||||
project
|
||||
repo
|
||||
repo-forked
|
||||
shield
|
||||
shield-lock
|
||||
star
|
||||
star-fill
|
||||
tag
|
||||
tools
|
||||
workflow
|
||||