193 lines
7.3 KiB
Go
193 lines
7.3 KiB
Go
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/v79/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")
|
|
}
|
|
}
|
|
}
|