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
7 changes: 7 additions & 0 deletions pkg/errors/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package errors

const (
ErrContextMissingGitHubCtxErrors = "context does not contain GitHubCtxErrors"
ErrFailedToGetGitHubClient = "failed to get GitHub client"
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
87 changes: 49 additions & 38 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,49 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun
}

// GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository.
func tryRawContentFetch(ctx context.Context, getRawClient raw.GetRawClientFn, owner, repo, path string, opts *raw.ContentOpts) (*http.Response, error) {
rawClient, err := getRawClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub raw content client: %w", err)
}
return rawClient.GetRawContent(ctx, owner, repo, path, opts)
}

func buildResourceURI(owner, repo, path, sha, ref string) (string, error) {
var resourceURI string
var err error

switch {
case sha != "":
resourceURI, err = url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path)
case ref != "":
resourceURI, err = url.JoinPath("repo://", owner, repo, ref, "contents", path)
default:
resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path)
}

if err != nil {
return "", fmt.Errorf("failed to create resource URI: %w", err)
}
return resourceURI, nil
}

func createResourceContent(body []byte, contentType, uri string) (*mcp.CallToolResult, error) {
if strings.HasPrefix(contentType, "application") || strings.HasPrefix(contentType, "text") {
return mcp.NewToolResultResource("successfully downloaded text file", mcp.TextResourceContents{
URI: uri,
Text: string(body),
MIMEType: contentType,
}), nil
}

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

func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_file_contents",
mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")),
Expand Down Expand Up @@ -523,12 +566,7 @@ 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)
resp, err := tryRawContentFetch(ctx, getRawClient, owner, repo, path, rawOpts)
if err != nil {
return mcp.NewToolResultError("failed to get raw repository content"), nil
}
Expand All @@ -542,41 +580,14 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
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
resourceURI, err := buildResourceURI(owner, repo, path, sha, ref)
if err != nil {
return nil, err
}

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

contentType := resp.Header.Get("Content-Type")
return createResourceContent(body, contentType, resourceURI)
}
}

Expand Down
5 changes: 3 additions & 2 deletions pkg/github/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"

pkgerrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/google/go-github/v72/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
Expand Down Expand Up @@ -70,7 +71,7 @@ func RequiredParam[T comparable](r mcp.CallToolRequest, p string) (T, error) {

// Check if the parameter is present in the request
if _, ok := r.GetArguments()[p]; !ok {
return zero, fmt.Errorf("missing required parameter: %s", p)
return zero, fmt.Errorf(pkgerrors.ErrMissingRequiredParameter, p)
}

// Check if the parameter is of the expected type
Expand All @@ -80,7 +81,7 @@ func RequiredParam[T comparable](r mcp.CallToolRequest, p string) (T, error) {
}

if val == zero {
return zero, fmt.Errorf("missing required parameter: %s", p)
return zero, fmt.Errorf(pkgerrors.ErrMissingRequiredParameter, p)
}

return val, nil
Expand Down