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") } } }