Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pkg/errors/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package errors

// Error message constants to avoid string duplication (SonarQube rule go:S1192)
const (
// ErrContextMissingGitHubCtxErrors is returned when the context does not contain GitHubCtxErrors
ErrContextMissingGitHubCtxErrors = "context does not contain GitHubCtxErrors"

// ErrFailedToGetGitHubClient is returned when getting the GitHub client fails
ErrFailedToGetGitHubClient = "failed to get GitHub client"

// ErrMissingRequiredParameter is a format string for missing required parameter errors
ErrMissingRequiredParameter = "missing required parameter: %s"
)
9 changes: 5 additions & 4 deletions pkg/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package errors

import (
"context"
"errors"
"fmt"

"github.com/google/go-github/v72/github"
Expand Down Expand Up @@ -71,15 +72,15 @@ func GetGitHubAPIErrors(ctx context.Context) ([]*GitHubAPIError, error) {
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
return val.api, nil // return the slice of API errors from the context
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
return nil, errors.New(ErrContextMissingGitHubCtxErrors)
}

// GetGitHubGraphQLErrors retrieves the slice of GitHubGraphQLErrors from the context.
func GetGitHubGraphQLErrors(ctx context.Context) ([]*GitHubGraphQLError, error) {
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
return val.graphQL, nil // return the slice of GraphQL errors from the context
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
return nil, errors.New(ErrContextMissingGitHubCtxErrors)
}

func NewGitHubAPIErrorToCtx(ctx context.Context, message string, resp *github.Response, err error) (context.Context, error) {
Expand All @@ -95,15 +96,15 @@ func addGitHubAPIErrorToContext(ctx context.Context, err *GitHubAPIError) (conte
val.api = append(val.api, err) // append the error to the existing slice in the context
return ctx, nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
return nil, errors.New(ErrContextMissingGitHubCtxErrors)
}

func addGitHubGraphQLErrorToContext(ctx context.Context, err *GitHubGraphQLError) (context.Context, error) {
if val, ok := ctx.Value(GitHubErrorKey{}).(*GitHubCtxErrors); ok {
val.graphQL = append(val.graphQL, err) // append the error to the existing slice in the context
return ctx, nil
}
return nil, fmt.Errorf("context does not contain GitHubCtxErrors")
return nil, errors.New(ErrContextMissingGitHubCtxErrors)
}

// NewGitHubAPIErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
Expand Down
119 changes: 65 additions & 54 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,69 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun
}
}

// buildResourceURI constructs the resource URI based on the provided parameters.
// This helper function reduces cognitive complexity by extracting URI building logic.
func buildResourceURI(owner, repo, path, sha, ref string) (string, error) {
switch {
case sha != "":
return url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path)
case ref != "":
return url.JoinPath("repo://", owner, repo, ref, "contents", path)
default:
return url.JoinPath("repo://", owner, repo, "contents", path)
}
}

// createResourceContent creates the appropriate MCP resource content based on content type.
// This helper function reduces cognitive complexity by extracting content creation logic.
func createResourceContent(body []byte, contentType, resourceURI string) *mcp.CallToolResult {
if strings.HasPrefix(contentType, "application") || strings.HasPrefix(contentType, "text") {
return mcp.NewToolResultResource("successfully downloaded text file", mcp.TextResourceContents{
URI: resourceURI,
Text: string(body),
MIMEType: contentType,
})
}
return mcp.NewToolResultResource("successfully downloaded binary file", mcp.BlobResourceContents{
URI: resourceURI,
Blob: base64.StdEncoding.EncodeToString(body),
MIMEType: contentType,
})
}

// fetchRawContent attempts to fetch raw content from GitHub and returns the result.
// This helper function reduces cognitive complexity by extracting raw content fetching logic.
func fetchRawContent(ctx context.Context, getRawClient raw.GetRawClientFn, owner, repo, path string, rawOpts *raw.ContentOpts) (*mcp.CallToolResult, bool) {
rawClient, err := getRawClient(ctx)
if err != nil {
return mcp.NewToolResultError("failed to get GitHub raw content client"), true
}
resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
if err != nil {
return mcp.NewToolResultError("failed to get raw repository content"), true
}
defer func() {
_ = resp.Body.Close()
}()

if resp.StatusCode != http.StatusOK {
return nil, false
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return mcp.NewToolResultError("failed to read response body"), true
}

contentType := resp.Header.Get("Content-Type")
resourceURI, err := buildResourceURI(owner, repo, path, rawOpts.SHA, rawOpts.Ref)
if err != nil {
return nil, true
}

return createResourceContent(body, contentType, resourceURI), true
}

// GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository.
func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_file_contents",
Expand Down Expand Up @@ -523,60 +586,8 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t

// If the path is (most likely) not to be a directory, we will first try to get the raw content from the GitHub raw content API.
if path != "" && !strings.HasSuffix(path, "/") {

rawClient, err := getRawClient(ctx)
if err != nil {
return mcp.NewToolResultError("failed to get GitHub raw content client"), nil
}
resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
if err != nil {
return mcp.NewToolResultError("failed to get raw repository content"), nil
}
defer func() {
_ = resp.Body.Close()
}()

if resp.StatusCode == http.StatusOK {
// If the raw content is found, return it directly
body, err := io.ReadAll(resp.Body)
if err != nil {
return mcp.NewToolResultError("failed to read response body"), nil
}
contentType := resp.Header.Get("Content-Type")

var resourceURI string
switch {
case sha != "":
resourceURI, err = url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path)
if err != nil {
return nil, fmt.Errorf("failed to create resource URI: %w", err)
}
case ref != "":
resourceURI, err = url.JoinPath("repo://", owner, repo, ref, "contents", path)
if err != nil {
return nil, fmt.Errorf("failed to create resource URI: %w", err)
}
default:
resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path)
if err != nil {
return nil, fmt.Errorf("failed to create resource URI: %w", err)
}
}

if strings.HasPrefix(contentType, "application") || strings.HasPrefix(contentType, "text") {
return mcp.NewToolResultResource("successfully downloaded text file", mcp.TextResourceContents{
URI: resourceURI,
Text: string(body),
MIMEType: contentType,
}), nil
}

return mcp.NewToolResultResource("successfully downloaded binary file", mcp.BlobResourceContents{
URI: resourceURI,
Blob: base64.StdEncoding.EncodeToString(body),
MIMEType: contentType,
}), nil

if result, handled := fetchRawContent(ctx, getRawClient, owner, repo, path, rawOpts); handled {
return result, nil
}
}

Expand Down