Files
Sam Morrow 6b3c375492 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
2025-12-17 17:31:13 +01:00

135 lines
5.6 KiB
Go

package inventory
import (
"context"
"encoding/json"
"github.com/github/github-mcp-server/pkg/octicons"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// HandlerFunc is a function that takes dependencies and returns an MCP tool handler.
// This allows tools to be defined statically while their handlers are generated
// on-demand with the appropriate dependencies.
// The deps parameter is typed as `any` to avoid circular dependencies - callers
// should define their own typed dependencies struct and type-assert as needed.
type HandlerFunc func(deps any) mcp.ToolHandler
// ToolsetID is a unique identifier for a toolset.
// Using a distinct type provides compile-time type safety.
type ToolsetID string
// ToolsetMetadata contains metadata about the toolset a tool belongs to.
type ToolsetMetadata struct {
// ID is the unique identifier for the toolset (e.g., "repos", "issues")
ID ToolsetID
// Description provides a human-readable description of the toolset
Description string
// Default indicates this toolset should be enabled by default
Default bool
// Icon is the name of the Octicon to use for tools in this toolset.
// Use the base name without size suffix, e.g., "repo" not "repo-16".
// See https://primer.style/foundations/icons for available icons.
Icon string
}
// Icons returns MCP Icon objects for this toolset, or nil if no icon is set.
// Icons are provided in both 16x16 and 24x24 sizes.
func (tm ToolsetMetadata) Icons() []mcp.Icon {
return octicons.Icons(tm.Icon)
}
// ServerTool represents an MCP tool with metadata and a handler generator function.
// The tool definition is static, while the handler is generated on-demand
// when the tool is registered with a server.
// Tools are now self-describing with their toolset membership and read-only status
// derived from the Tool.Annotations.ReadOnlyHint field.
type ServerTool struct {
// Tool is the MCP tool definition containing name, description, schema, etc.
Tool mcp.Tool
// Toolset contains metadata about which toolset this tool belongs to.
Toolset ToolsetMetadata
// HandlerFunc generates the handler when given dependencies.
// This allows tools to be passed around without handlers being set up,
// and handlers are only created when needed.
HandlerFunc HandlerFunc
// FeatureFlagEnable specifies a feature flag that must be enabled for this tool
// to be available. If set and the flag is not enabled, the tool is omitted.
FeatureFlagEnable string
// FeatureFlagDisable specifies a feature flag that, when enabled, causes this tool
// to be omitted. Used to disable tools when a feature flag is on.
FeatureFlagDisable string
// Enabled is an optional function called at build/filter time to determine
// if this tool should be available. If nil, the tool is considered enabled
// (subject to FeatureFlagEnable/FeatureFlagDisable checks).
// The context carries request-scoped information for the consumer to use.
// Returns (enabled, error). On error, the tool should be treated as disabled.
Enabled func(ctx context.Context) (bool, error)
}
// IsReadOnly returns true if this tool is marked as read-only via annotations.
func (st *ServerTool) IsReadOnly() bool {
return st.Tool.Annotations != nil && st.Tool.Annotations.ReadOnlyHint
}
// HasHandler returns true if this tool has a handler function.
func (st *ServerTool) HasHandler() bool {
return st.HandlerFunc != nil
}
// Handler returns a tool handler by calling HandlerFunc with the given dependencies.
// Panics if HandlerFunc is nil - all tools should have handlers.
func (st *ServerTool) Handler(deps any) mcp.ToolHandler {
if st.HandlerFunc == nil {
panic("HandlerFunc is nil for tool: " + st.Tool.Name)
}
return st.HandlerFunc(deps)
}
// RegisterFunc registers the tool with the server using the provided dependencies.
// Icons are automatically applied from the toolset metadata if not already set.
// A shallow copy of the tool is made to avoid mutating the original ServerTool.
// Panics if the tool has no handler - all tools should have handlers.
func (st *ServerTool) RegisterFunc(s *mcp.Server, deps any) {
handler := st.Handler(deps) // This will panic if HandlerFunc is nil
// Make a shallow copy of the tool to avoid mutating the original
toolCopy := st.Tool
// Apply icons from toolset metadata if tool doesn't have icons set
if len(toolCopy.Icons) == 0 {
toolCopy.Icons = st.Toolset.Icons()
}
s.AddTool(&toolCopy, handler)
}
// NewServerTool creates a ServerTool from a tool definition, toolset metadata, and a typed handler function.
// The handler function takes dependencies (as any) and returns a typed handler.
// Callers should type-assert deps to their typed dependencies struct.
func NewServerTool[In any, Out any](tool mcp.Tool, toolset ToolsetMetadata, handlerFn func(deps any) mcp.ToolHandlerFor[In, Out]) ServerTool {
return ServerTool{
Tool: tool,
Toolset: toolset,
HandlerFunc: func(deps any) mcp.ToolHandler {
typedHandler := handlerFn(deps)
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var arguments In
if err := json.Unmarshal(req.Params.Arguments, &arguments); err != nil {
return nil, err
}
resp, _, err := typedHandler(ctx, req, arguments)
return resp, err
}
},
}
}
// NewServerToolFromHandler creates a ServerTool from a tool definition, toolset metadata, and a raw handler function.
// Use this when you have a handler that already conforms to mcp.ToolHandler.
func NewServerToolFromHandler(tool mcp.Tool, toolset ToolsetMetadata, handlerFn func(deps any) mcp.ToolHandler) ServerTool {
return ServerTool{Tool: tool, Toolset: toolset, HandlerFunc: handlerFn}
}