6b3c375492
* 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
259 lines
9.0 KiB
Go
259 lines
9.0 KiB
Go
package github
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/github/github-mcp-server/pkg/inventory"
|
|
"github.com/github/github-mcp-server/pkg/octicons"
|
|
"github.com/github/github-mcp-server/pkg/raw"
|
|
"github.com/github/github-mcp-server/pkg/translations"
|
|
"github.com/google/go-github/v79/github"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
"github.com/yosida95/uritemplate/v3"
|
|
)
|
|
|
|
var (
|
|
repositoryResourceContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/contents{/path*}")
|
|
repositoryResourceBranchContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}")
|
|
repositoryResourceCommitContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/sha/{sha}/contents{/path*}")
|
|
repositoryResourceTagContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}")
|
|
repositoryResourcePrContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}")
|
|
)
|
|
|
|
// GetRepositoryResourceContent defines the resource template for getting repository content.
|
|
func GetRepositoryResourceContent(t translations.TranslationHelperFunc) inventory.ServerResourceTemplate {
|
|
return inventory.NewServerResourceTemplate(
|
|
ToolsetMetadataRepos,
|
|
mcp.ResourceTemplate{
|
|
Name: "repository_content",
|
|
URITemplate: repositoryResourceContentURITemplate.Raw(),
|
|
Description: t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"),
|
|
Icons: octicons.Icons("repo"),
|
|
},
|
|
repositoryResourceContentsHandlerFunc(repositoryResourceContentURITemplate),
|
|
)
|
|
}
|
|
|
|
// GetRepositoryResourceBranchContent defines the resource template for getting repository content for a branch.
|
|
func GetRepositoryResourceBranchContent(t translations.TranslationHelperFunc) inventory.ServerResourceTemplate {
|
|
return inventory.NewServerResourceTemplate(
|
|
ToolsetMetadataRepos,
|
|
mcp.ResourceTemplate{
|
|
Name: "repository_content_branch",
|
|
URITemplate: repositoryResourceBranchContentURITemplate.Raw(),
|
|
Description: t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"),
|
|
Icons: octicons.Icons("git-branch"),
|
|
},
|
|
repositoryResourceContentsHandlerFunc(repositoryResourceBranchContentURITemplate),
|
|
)
|
|
}
|
|
|
|
// GetRepositoryResourceCommitContent defines the resource template for getting repository content for a commit.
|
|
func GetRepositoryResourceCommitContent(t translations.TranslationHelperFunc) inventory.ServerResourceTemplate {
|
|
return inventory.NewServerResourceTemplate(
|
|
ToolsetMetadataRepos,
|
|
mcp.ResourceTemplate{
|
|
Name: "repository_content_commit",
|
|
URITemplate: repositoryResourceCommitContentURITemplate.Raw(),
|
|
Description: t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"),
|
|
Icons: octicons.Icons("git-commit"),
|
|
},
|
|
repositoryResourceContentsHandlerFunc(repositoryResourceCommitContentURITemplate),
|
|
)
|
|
}
|
|
|
|
// GetRepositoryResourceTagContent defines the resource template for getting repository content for a tag.
|
|
func GetRepositoryResourceTagContent(t translations.TranslationHelperFunc) inventory.ServerResourceTemplate {
|
|
return inventory.NewServerResourceTemplate(
|
|
ToolsetMetadataRepos,
|
|
mcp.ResourceTemplate{
|
|
Name: "repository_content_tag",
|
|
URITemplate: repositoryResourceTagContentURITemplate.Raw(),
|
|
Description: t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"),
|
|
Icons: octicons.Icons("tag"),
|
|
},
|
|
repositoryResourceContentsHandlerFunc(repositoryResourceTagContentURITemplate),
|
|
)
|
|
}
|
|
|
|
// GetRepositoryResourcePrContent defines the resource template for getting repository content for a pull request.
|
|
func GetRepositoryResourcePrContent(t translations.TranslationHelperFunc) inventory.ServerResourceTemplate {
|
|
return inventory.NewServerResourceTemplate(
|
|
ToolsetMetadataRepos,
|
|
mcp.ResourceTemplate{
|
|
Name: "repository_content_pr",
|
|
URITemplate: repositoryResourcePrContentURITemplate.Raw(),
|
|
Description: t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"),
|
|
Icons: octicons.Icons("git-pull-request"),
|
|
},
|
|
repositoryResourceContentsHandlerFunc(repositoryResourcePrContentURITemplate),
|
|
)
|
|
}
|
|
|
|
// repositoryResourceContentsHandlerFunc returns a ResourceHandlerFunc that creates handlers on-demand.
|
|
func repositoryResourceContentsHandlerFunc(resourceURITemplate *uritemplate.Template) inventory.ResourceHandlerFunc {
|
|
return func(deps any) mcp.ResourceHandler {
|
|
d := deps.(ToolDependencies)
|
|
return RepositoryResourceContentsHandler(d, resourceURITemplate)
|
|
}
|
|
}
|
|
|
|
// RepositoryResourceContentsHandler returns a handler function for repository content requests.
|
|
func RepositoryResourceContentsHandler(deps ToolDependencies, resourceURITemplate *uritemplate.Template) mcp.ResourceHandler {
|
|
return func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
|
// Match the URI to extract parameters
|
|
uriValues := resourceURITemplate.Match(request.Params.URI)
|
|
if uriValues == nil {
|
|
return nil, fmt.Errorf("failed to match URI: %s", request.Params.URI)
|
|
}
|
|
|
|
// Extract required vars
|
|
owner := uriValues.Get("owner").String()
|
|
repo := uriValues.Get("repo").String()
|
|
|
|
if owner == "" {
|
|
return nil, errors.New("owner is required")
|
|
}
|
|
|
|
if repo == "" {
|
|
return nil, errors.New("repo is required")
|
|
}
|
|
|
|
pathValue := uriValues.Get("path")
|
|
pathComponents := pathValue.List()
|
|
var path string
|
|
|
|
if len(pathComponents) == 0 {
|
|
path = pathValue.String()
|
|
} else {
|
|
path = strings.Join(pathComponents, "/")
|
|
}
|
|
|
|
opts := &github.RepositoryContentGetOptions{}
|
|
rawOpts := &raw.ContentOpts{}
|
|
|
|
sha := uriValues.Get("sha").String()
|
|
if sha != "" {
|
|
opts.Ref = sha
|
|
rawOpts.SHA = sha
|
|
}
|
|
|
|
branch := uriValues.Get("branch").String()
|
|
if branch != "" {
|
|
opts.Ref = "refs/heads/" + branch
|
|
rawOpts.Ref = "refs/heads/" + branch
|
|
}
|
|
|
|
tag := uriValues.Get("tag").String()
|
|
if tag != "" {
|
|
opts.Ref = "refs/tags/" + tag
|
|
rawOpts.Ref = "refs/tags/" + tag
|
|
}
|
|
|
|
prNumber := uriValues.Get("prNumber").String()
|
|
if prNumber != "" {
|
|
// fetch the PR from the API to get the latest commit and use SHA
|
|
githubClient, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
|
}
|
|
prNum, err := strconv.Atoi(prNumber)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid pull request number: %w", err)
|
|
}
|
|
pr, _, err := githubClient.PullRequests.Get(ctx, owner, repo, prNum)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get pull request: %w", err)
|
|
}
|
|
sha := pr.GetHead().GetSHA()
|
|
rawOpts.SHA = sha
|
|
opts.Ref = sha
|
|
}
|
|
// if it's a directory
|
|
if path == "" || strings.HasSuffix(path, "/") {
|
|
return nil, fmt.Errorf("directories are not supported: %s", path)
|
|
}
|
|
rawClient, err := deps.GetRawClient(ctx)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get GitHub raw content client: %w", err)
|
|
}
|
|
|
|
resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
|
|
defer func() {
|
|
_ = resp.Body.Close()
|
|
}()
|
|
// If the raw content is not found, we will fall back to the GitHub API (in case it is a directory)
|
|
switch {
|
|
case err != nil:
|
|
return nil, fmt.Errorf("failed to get raw content: %w", err)
|
|
case resp.StatusCode == http.StatusOK:
|
|
ext := filepath.Ext(path)
|
|
mimeType := resp.Header.Get("Content-Type")
|
|
if ext == ".md" {
|
|
mimeType = "text/markdown"
|
|
} else if mimeType == "" {
|
|
mimeType = mime.TypeByExtension(ext)
|
|
}
|
|
|
|
content, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read file content: %w", err)
|
|
}
|
|
|
|
switch {
|
|
case strings.HasPrefix(mimeType, "text"), strings.HasPrefix(mimeType, "application"):
|
|
return &mcp.ReadResourceResult{
|
|
Contents: []*mcp.ResourceContents{
|
|
{
|
|
URI: request.Params.URI,
|
|
MIMEType: mimeType,
|
|
Text: string(content),
|
|
},
|
|
},
|
|
}, nil
|
|
default:
|
|
var buf bytes.Buffer
|
|
base64Encoder := base64.NewEncoder(base64.StdEncoding, &buf)
|
|
_, err := base64Encoder.Write(content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to base64 encode content: %w", err)
|
|
}
|
|
if err := base64Encoder.Close(); err != nil {
|
|
return nil, fmt.Errorf("failed to close base64 encoder: %w", err)
|
|
}
|
|
|
|
return &mcp.ReadResourceResult{
|
|
Contents: []*mcp.ResourceContents{
|
|
{
|
|
URI: request.Params.URI,
|
|
MIMEType: mimeType,
|
|
Blob: buf.Bytes(),
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
case resp.StatusCode != http.StatusNotFound:
|
|
// If we got a response but it is not 200 OK, we return an error
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
return nil, fmt.Errorf("failed to fetch raw content: %s", string(body))
|
|
default:
|
|
// This should be unreachable because GetContents should return an error if neither file nor directory content is found.
|
|
return nil, errors.New("404 Not Found")
|
|
}
|
|
}
|
|
}
|