github-mcp-server / pkg /github /repository_resource.go
Gemini
Initial commit
fce10de
package github
import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v74/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// GetRepositoryResourceContent defines the resource template and handler for getting repository content.
func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
}
// GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch.
func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
}
// GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit.
func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
}
// GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag.
func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
}
// GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request.
func GetRepositoryResourcePrContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
}
// RepositoryResourceContentsHandler returns a handler function for repository content requests.
func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.GetRawClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// the matcher will give []string with one element
// https://github.com/mark3labs/mcp-go/pull/54
o, ok := request.Params.Arguments["owner"].([]string)
if !ok || len(o) == 0 {
return nil, errors.New("owner is required")
}
owner := o[0]
r, ok := request.Params.Arguments["repo"].([]string)
if !ok || len(r) == 0 {
return nil, errors.New("repo is required")
}
repo := r[0]
// path should be a joined list of the path parts
path := ""
p, ok := request.Params.Arguments["path"].([]string)
if ok {
path = strings.Join(p, "/")
}
opts := &github.RepositoryContentGetOptions{}
rawOpts := &raw.ContentOpts{}
sha, ok := request.Params.Arguments["sha"].([]string)
if ok && len(sha) > 0 {
opts.Ref = sha[0]
rawOpts.SHA = sha[0]
}
branch, ok := request.Params.Arguments["branch"].([]string)
if ok && len(branch) > 0 {
opts.Ref = "refs/heads/" + branch[0]
rawOpts.Ref = "refs/heads/" + branch[0]
}
tag, ok := request.Params.Arguments["tag"].([]string)
if ok && len(tag) > 0 {
opts.Ref = "refs/tags/" + tag[0]
rawOpts.Ref = "refs/tags/" + tag[0]
}
prNumber, ok := request.Params.Arguments["prNumber"].([]string)
if ok && len(prNumber) > 0 {
// fetch the PR from the API to get the latest commit and use SHA
githubClient, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
prNum, err := strconv.Atoi(prNumber[0])
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 := 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.ResourceContents{
mcp.TextResourceContents{
URI: request.Params.URI,
MIMEType: mimeType,
Text: string(content),
},
}, nil
default:
return []mcp.ResourceContents{
mcp.BlobResourceContents{
URI: request.Params.URI,
MIMEType: mimeType,
Blob: base64.StdEncoding.EncodeToString(content),
},
}, 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")
}
}
}