From 383c2d2f032cfb63a652437c6cc169c4d3a9f70b Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Tue, 1 Apr 2025 19:18:50 -0700 Subject: [PATCH 01/11] refactor to make testing easier --- pkg/github/repository_resource.go | 179 +++++++++++++++++------------- pkg/github/server.go | 12 +- 2 files changed, 108 insertions(+), 83 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 1aad08db2..14c535569 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -14,109 +14,136 @@ import ( ) // getRepositoryContent defines the resource template and handler for the Repository Content API. -func getRepositoryContent(client *github.Client, t translations.TranslationHelperFunc) (mainTemplate mcp.ResourceTemplate, reftemplate mcp.ResourceTemplate, shaTemplate mcp.ResourceTemplate, tagTemplate mcp.ResourceTemplate, prTemplate mcp.ResourceTemplate, handler server.ResourceTemplateHandlerFunc) { - +func getRepositoryContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), - ), mcp.NewResourceTemplate( + ), + handlerFunc(client, t) +} + +// getRepositoryContent defines the resource template and handler for the Repository Content API. +func getRepositoryBranchContent(client *github.Client, 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"), - ), mcp.NewResourceTemplate( + ), + handlerFunc(client, t) +} + +// getRepositoryContent defines the resource template and handler for the Repository Content API. +func getRepositoryCommitContent(client *github.Client, 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"), - ), mcp.NewResourceTemplate( + ), + handlerFunc(client, t) +} + +// getRepositoryContent defines the resource template and handler for the Repository Content API. +func getRepositoryTagContent(client *github.Client, 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"), - ), mcp.NewResourceTemplate( + ), + handlerFunc(client, t) +} + +// getRepositoryContent defines the resource template and handler for the Repository Content API. +func getRepositoryPrContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { + return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), - ), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - // Extract parameters from request.Params.URI + ), + handlerFunc(client, t) +} - owner := request.Params.Arguments["owner"].([]string)[0] - repo := request.Params.Arguments["repo"].([]string)[0] - // path should be a joined list of the path parts - path := strings.Join(request.Params.Arguments["path"].([]string), "/") +func handlerFunc(client *github.Client, _ translations.TranslationHelperFunc) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { // Extract parameters from request.Params.URI - opts := &github.RepositoryContentGetOptions{} + owner := request.Params.Arguments["owner"].([]string)[0] + repo := request.Params.Arguments["repo"].([]string)[0] + // path should be a joined list of the path parts + path := strings.Join(request.Params.Arguments["path"].([]string), "/") - sha, ok := request.Params.Arguments["sha"].([]string) - if ok { - opts.Ref = sha[0] - } + opts := &github.RepositoryContentGetOptions{} - branch, ok := request.Params.Arguments["branch"].([]string) - if ok { - opts.Ref = "refs/heads/" + branch[0] - } + sha, ok := request.Params.Arguments["sha"].([]string) + if ok { + opts.Ref = sha[0] + } - tag, ok := request.Params.Arguments["tag"].([]string) - if ok { - opts.Ref = "refs/tags/" + tag[0] - } - prNumber, ok := request.Params.Arguments["pr_number"].([]string) - if ok { - opts.Ref = "refs/pull/" + prNumber[0] + "/head" - } + branch, ok := request.Params.Arguments["branch"].([]string) + if ok { + opts.Ref = "refs/heads/" + branch[0] + } + + tag, ok := request.Params.Arguments["tag"].([]string) + if ok { + opts.Ref = "refs/tags/" + tag[0] + } + prNumber, ok := request.Params.Arguments["pr_number"].([]string) + if ok { + opts.Ref = "refs/pull/" + prNumber[0] + "/head" + } + + // Use the GitHub client to fetch repository content + fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) + if err != nil { + return nil, err + } + + if directoryContent != nil { + // Process the directory content and return it as resource contents + var resources []mcp.ResourceContents + for _, entry := range directoryContent { + mimeType := "text/directory" + if entry.GetType() == "file" { + mimeType = mime.TypeByExtension(filepath.Ext(entry.GetName())) + } + resources = append(resources, mcp.TextResourceContents{ + URI: entry.GetHTMLURL(), + MIMEType: mimeType, + Text: entry.GetName(), + }) - // Use the GitHub client to fetch repository content - fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) - if err != nil { - return nil, err } + return resources, nil - if directoryContent != nil { - // Process the directory content and return it as resource contents - var resources []mcp.ResourceContents - for _, entry := range directoryContent { - mimeType := "text/directory" - if entry.GetType() == "file" { - mimeType = mime.TypeByExtension(filepath.Ext(entry.GetName())) - } - resources = append(resources, mcp.TextResourceContents{ - URI: entry.GetHTMLURL(), - MIMEType: mimeType, - Text: entry.GetName(), - }) + } else if fileContent != nil { + // Process the file content and return it as a binary resource + if fileContent.Content != nil { + decodedContent, err := fileContent.GetContent() + if err != nil { + return nil, err } - return resources, nil - - } else if fileContent != nil { - // Process the file content and return it as a binary resource - - if fileContent.Content != nil { - decodedContent, err := fileContent.GetContent() - if err != nil { - return nil, err - } - - mimeType := mime.TypeByExtension(filepath.Ext(fileContent.GetName())) - - // Check if the file is text-based - if strings.HasPrefix(mimeType, "text") { - // Return as TextResourceContents - return []mcp.ResourceContents{ - mcp.TextResourceContents{ - URI: request.Params.URI, - MIMEType: mimeType, - Text: decodedContent, - }, - }, nil - } - - // Otherwise, return as BlobResourceContents + + mimeType := mime.TypeByExtension(filepath.Ext(fileContent.GetName())) + + // Check if the file is text-based + if strings.HasPrefix(mimeType, "text") { + // Return as TextResourceContents return []mcp.ResourceContents{ - mcp.BlobResourceContents{ + mcp.TextResourceContents{ URI: request.Params.URI, MIMEType: mimeType, - Blob: base64.StdEncoding.EncodeToString([]byte(decodedContent)), // Encode content as Base64 + Text: decodedContent, }, }, nil } - } - return nil, nil + // Otherwise, return as BlobResourceContents + return []mcp.ResourceContents{ + mcp.BlobResourceContents{ + URI: request.Params.URI, + MIMEType: mimeType, + Blob: base64.StdEncoding.EncodeToString([]byte(decodedContent)), // Encode content as Base64 + }, + }, nil + } } + + return nil, nil + } } diff --git a/pkg/github/server.go b/pkg/github/server.go index ce39c87e9..82d273676 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -25,13 +25,11 @@ func NewServer(client *github.Client, readOnly bool, t translations.TranslationH server.WithLogging()) // Add GitHub Resources - defaultTemplate, branchTemplate, tagTemplate, shaTemplate, prTemplate, handler := getRepositoryContent(client, t) - - s.AddResourceTemplate(defaultTemplate, handler) - s.AddResourceTemplate(branchTemplate, handler) - s.AddResourceTemplate(tagTemplate, handler) - s.AddResourceTemplate(shaTemplate, handler) - s.AddResourceTemplate(prTemplate, handler) + s.AddResourceTemplate(getRepositoryContent(client, t)) + s.AddResourceTemplate(getRepositoryBranchContent(client, t)) + s.AddResourceTemplate(getRepositoryCommitContent(client, t)) + s.AddResourceTemplate(getRepositoryTagContent(client, t)) + s.AddResourceTemplate(getRepositoryPrContent(client, t)) // Add GitHub tools - Issues s.AddTool(getIssue(client, t)) From 1cb52f9b884aea348432c29d14ff97d5737ba918 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Tue, 1 Apr 2025 19:35:05 -0700 Subject: [PATCH 02/11] not needed in handler func --- pkg/github/repository_resource.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 14c535569..7589a1a6f 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -19,7 +19,7 @@ func getRepositoryContent(client *github.Client, t translations.TranslationHelpe "repo://{owner}/{repo}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), ), - handlerFunc(client, t) + handlerFunc(client) } // getRepositoryContent defines the resource template and handler for the Repository Content API. @@ -28,7 +28,7 @@ func getRepositoryBranchContent(client *github.Client, t translations.Translatio "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"), ), - handlerFunc(client, t) + handlerFunc(client) } // getRepositoryContent defines the resource template and handler for the Repository Content API. @@ -37,7 +37,7 @@ func getRepositoryCommitContent(client *github.Client, t translations.Translatio "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"), ), - handlerFunc(client, t) + handlerFunc(client) } // getRepositoryContent defines the resource template and handler for the Repository Content API. @@ -46,7 +46,7 @@ func getRepositoryTagContent(client *github.Client, t translations.TranslationHe "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"), ), - handlerFunc(client, t) + handlerFunc(client) } // getRepositoryContent defines the resource template and handler for the Repository Content API. @@ -55,10 +55,10 @@ func getRepositoryPrContent(client *github.Client, t translations.TranslationHel "repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), ), - handlerFunc(client, t) + handlerFunc(client) } -func handlerFunc(client *github.Client, _ translations.TranslationHelperFunc) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { +func handlerFunc(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { // Extract parameters from request.Params.URI owner := request.Params.Arguments["owner"].([]string)[0] From 2aa3002f1826bed79fcca38325aa89ba8a8ff37c Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Tue, 1 Apr 2025 21:29:38 -0700 Subject: [PATCH 03/11] small cleanup --- pkg/github/repository_resource.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 7589a1a6f..806fc9c19 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -19,7 +19,7 @@ func getRepositoryContent(client *github.Client, t translations.TranslationHelpe "repo://{owner}/{repo}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), ), - handlerFunc(client) + repoContentsResourceHandler(client) } // getRepositoryContent defines the resource template and handler for the Repository Content API. @@ -28,7 +28,7 @@ func getRepositoryBranchContent(client *github.Client, t translations.Translatio "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"), ), - handlerFunc(client) + repoContentsResourceHandler(client) } // getRepositoryContent defines the resource template and handler for the Repository Content API. @@ -37,7 +37,7 @@ func getRepositoryCommitContent(client *github.Client, t translations.Translatio "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"), ), - handlerFunc(client) + repoContentsResourceHandler(client) } // getRepositoryContent defines the resource template and handler for the Repository Content API. @@ -46,7 +46,7 @@ func getRepositoryTagContent(client *github.Client, t translations.TranslationHe "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"), ), - handlerFunc(client) + repoContentsResourceHandler(client) } // getRepositoryContent defines the resource template and handler for the Repository Content API. @@ -55,10 +55,10 @@ func getRepositoryPrContent(client *github.Client, t translations.TranslationHel "repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), ), - handlerFunc(client) + repoContentsResourceHandler(client) } -func handlerFunc(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { +func repoContentsResourceHandler(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { // Extract parameters from request.Params.URI owner := request.Params.Arguments["owner"].([]string)[0] @@ -110,7 +110,8 @@ func handlerFunc(client *github.Client) func(ctx context.Context, request mcp.Re } return resources, nil - } else if fileContent != nil { + } + if fileContent != nil { // Process the file content and return it as a binary resource if fileContent.Content != nil { From d8b0056998c621ab9be8678259a5da79d11b5c15 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Tue, 1 Apr 2025 22:21:16 -0700 Subject: [PATCH 04/11] create repository_resource_test --- pkg/github/repository_resource_test.go | 172 +++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 pkg/github/repository_resource_test.go diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go new file mode 100644 index 000000000..6b75f04cf --- /dev/null +++ b/pkg/github/repository_resource_test.go @@ -0,0 +1,172 @@ +package github + +import ( + "context" + "encoding/base64" + "net/http" + "testing" + + "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_RepoContentsResourceHandler(t *testing.T) { + mockDirContent := []*github.RepositoryContent{ + { + Type: github.Ptr("file"), + Name: github.Ptr("README.md"), + Path: github.Ptr("README.md"), + SHA: github.Ptr("abc123"), + Size: github.Ptr(42), + HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), + }, + { + Type: github.Ptr("dir"), + Name: github.Ptr("src"), + Path: github.Ptr("src"), + SHA: github.Ptr("def456"), + HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"), + }, + } + expectedDirContent := []mcp.TextResourceContents{ + { + URI: "https://github.com/owner/repo/blob/main/README.md", + MIMEType: "", + Text: "README.md", + }, + { + URI: "https://github.com/owner/repo/tree/main/src", + MIMEType: "text/directory", + Text: "src", + }, + } + + mockFileContent := &github.RepositoryContent{ + Type: github.Ptr("file"), + Name: github.Ptr("README.md"), + Path: github.Ptr("README.md"), + Content: github.Ptr("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku"), // Base64 encoded "# Test Repository\n\nThis is a test repository." + SHA: github.Ptr("abc123"), + Size: github.Ptr(42), + HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), + DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"), + } + + expectedFileContent := []mcp.BlobResourceContents{ + { + Blob: base64.StdEncoding.EncodeToString([]byte("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku")), // Base64 encoded "# Test Repository\n\nThis is a test repository." + + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedResult any + expectedErrMsg string + }{ + { + name: "successful file content fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposContentsByOwnerByRepoByPath, + mockFileContent, + ), + ), + requestArgs: map[string]any{ + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"README.md"}, + "branch": []string{"main"}, + }, + expectError: false, + expectedResult: expectedFileContent, + }, + { + name: "successful directory content fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposContentsByOwnerByRepoByPath, + mockDirContent, + ), + ), + requestArgs: map[string]any{ + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"src"}, + }, + expectError: false, + expectedResult: expectedDirContent, + }, + { + name: "empty content fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposContentsByOwnerByRepoByPath, + []*github.RepositoryContent{}, + ), + ), + requestArgs: map[string]any{ + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"src"}, + }, + expectError: false, + expectedResult: nil, + }, + { + name: "content fetch fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), + ), + requestArgs: map[string]any{ + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"nonexistent.md"}, + "branch": []string{"main"}, + }, + expectError: true, + expectedErrMsg: "404 Not Found", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + handler := repoContentsResourceHandler(client) + + // Create call request + request := mcp.ReadResourceRequest{ + Params: struct { + URI string `json:"uri"` + Arguments map[string]any `json:"arguments,omitempty"` + }{ + Arguments: tc.requestArgs, + }, + } + + resp, err := handler(context.TODO(), request) + + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + require.ElementsMatch(t, resp, tc.expectedResult) + }) + } +} From 2fdda7c7d2dbe7f891c707dfcd1ae2894aa6d7e5 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Tue, 1 Apr 2025 22:33:58 -0700 Subject: [PATCH 05/11] remove chatty comments --- pkg/github/repository_resource.go | 7 ------- pkg/github/repository_resource_test.go | 5 +---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 806fc9c19..3eaa04bbd 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -87,14 +87,12 @@ func repoContentsResourceHandler(client *github.Client) func(ctx context.Context opts.Ref = "refs/pull/" + prNumber[0] + "/head" } - // Use the GitHub client to fetch repository content fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) if err != nil { return nil, err } if directoryContent != nil { - // Process the directory content and return it as resource contents var resources []mcp.ResourceContents for _, entry := range directoryContent { mimeType := "text/directory" @@ -112,8 +110,6 @@ func repoContentsResourceHandler(client *github.Client) func(ctx context.Context } if fileContent != nil { - // Process the file content and return it as a binary resource - if fileContent.Content != nil { decodedContent, err := fileContent.GetContent() if err != nil { @@ -122,9 +118,7 @@ func repoContentsResourceHandler(client *github.Client) func(ctx context.Context mimeType := mime.TypeByExtension(filepath.Ext(fileContent.GetName())) - // Check if the file is text-based if strings.HasPrefix(mimeType, "text") { - // Return as TextResourceContents return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: request.Params.URI, @@ -134,7 +128,6 @@ func repoContentsResourceHandler(client *github.Client) func(ctx context.Context }, nil } - // Otherwise, return as BlobResourceContents return []mcp.ResourceContents{ mcp.BlobResourceContents{ URI: request.Params.URI, diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 6b75f04cf..01bcb8763 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -57,8 +57,7 @@ func Test_RepoContentsResourceHandler(t *testing.T) { expectedFileContent := []mcp.BlobResourceContents{ { - Blob: base64.StdEncoding.EncodeToString([]byte("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku")), // Base64 encoded "# Test Repository\n\nThis is a test repository." - + Blob: base64.StdEncoding.EncodeToString([]byte("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku")), }, } @@ -143,11 +142,9 @@ func Test_RepoContentsResourceHandler(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // Setup client with mock client := github.NewClient(tc.mockedClient) handler := repoContentsResourceHandler(client) - // Create call request request := mcp.ReadResourceRequest{ Params: struct { URI string `json:"uri"` From 9680b2468750e4340bada699537db96ca32b5cf4 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Tue, 1 Apr 2025 23:13:25 -0700 Subject: [PATCH 06/11] comment cleanup, function rename and some more tests --- pkg/github/repository_resource.go | 34 +++++++++++++------------- pkg/github/repository_resource_test.go | 29 ++++++++++++++++++++-- pkg/github/server.go | 10 ++++---- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 3eaa04bbd..5c87d59b2 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -13,53 +13,53 @@ import ( "github.com/mark3labs/mcp-go/server" ) -// getRepositoryContent defines the resource template and handler for the Repository Content API. -func getRepositoryContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// getRepositoryResourceContent defines the resource template and handler for getting repository content. +func getRepositoryResourceContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), ), - repoContentsResourceHandler(client) + repositoryResourceContentsHandler(client) } -// getRepositoryContent defines the resource template and handler for the Repository Content API. -func getRepositoryBranchContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// getRepositoryContent defines the resource template and handler for getting repository content for a branch. +func getRepositoryResourceBranchContent(client *github.Client, 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"), ), - repoContentsResourceHandler(client) + repositoryResourceContentsHandler(client) } -// getRepositoryContent defines the resource template and handler for the Repository Content API. -func getRepositoryCommitContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// getRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit. +func getRepositoryResourceCommitContent(client *github.Client, 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"), ), - repoContentsResourceHandler(client) + repositoryResourceContentsHandler(client) } -// getRepositoryContent defines the resource template and handler for the Repository Content API. -func getRepositoryTagContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// getRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag. +func getRepositoryResourceTagContent(client *github.Client, 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"), ), - repoContentsResourceHandler(client) + repositoryResourceContentsHandler(client) } -// getRepositoryContent defines the resource template and handler for the Repository Content API. -func getRepositoryPrContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// getRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request. +func getRepositoryResourcePrContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), ), - repoContentsResourceHandler(client) + repositoryResourceContentsHandler(client) } -func repoContentsResourceHandler(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { // Extract parameters from request.Params.URI +func repositoryResourceContentsHandler(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { owner := request.Params.Arguments["owner"].([]string)[0] repo := request.Params.Arguments["repo"].([]string)[0] diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 01bcb8763..a0419744f 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -6,6 +6,7 @@ import ( "net/http" "testing" + "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v69/github" "github.com/mark3labs/mcp-go/mcp" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -13,7 +14,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_RepoContentsResourceHandler(t *testing.T) { +func Test_repositoryResourceContentsHandler(t *testing.T) { mockDirContent := []*github.RepositoryContent{ { Type: github.Ptr("file"), @@ -143,7 +144,7 @@ func Test_RepoContentsResourceHandler(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - handler := repoContentsResourceHandler(client) + handler := repositoryResourceContentsHandler(client) request := mcp.ReadResourceRequest{ Params: struct { @@ -167,3 +168,27 @@ func Test_RepoContentsResourceHandler(t *testing.T) { }) } } + +func Test_getRepositoryResourceContent(t *testing.T) { + tmpl, _ := getRepositoryResourceContent(nil, translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/contents{/path*}", tmpl.URITemplate.Raw()) +} + +func Test_getRepositoryResourceBranchContent(t *testing.T) { + tmpl, _ := getRepositoryResourceBranchContent(nil, translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", tmpl.URITemplate.Raw()) +} +func Test_getRepositoryResourceCommitContent(t *testing.T) { + tmpl, _ := getRepositoryResourceCommitContent(nil, translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", tmpl.URITemplate.Raw()) +} + +func Test_getRepositoryResourceTagContent(t *testing.T) { + tmpl, _ := getRepositoryResourceTagContent(nil, translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", tmpl.URITemplate.Raw()) +} + +func Test_getRepositoryResourcePrContent(t *testing.T) { + tmpl, _ := getRepositoryResourcePrContent(nil, translations.NullTranslationHelper) + require.Equal(t, "repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}", tmpl.URITemplate.Raw()) +} diff --git a/pkg/github/server.go b/pkg/github/server.go index 82d273676..d652dde05 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -25,11 +25,11 @@ func NewServer(client *github.Client, readOnly bool, t translations.TranslationH server.WithLogging()) // Add GitHub Resources - s.AddResourceTemplate(getRepositoryContent(client, t)) - s.AddResourceTemplate(getRepositoryBranchContent(client, t)) - s.AddResourceTemplate(getRepositoryCommitContent(client, t)) - s.AddResourceTemplate(getRepositoryTagContent(client, t)) - s.AddResourceTemplate(getRepositoryPrContent(client, t)) + s.AddResourceTemplate(getRepositoryResourceContent(client, t)) + s.AddResourceTemplate(getRepositoryResourceBranchContent(client, t)) + s.AddResourceTemplate(getRepositoryResourceCommitContent(client, t)) + s.AddResourceTemplate(getRepositoryResourceTagContent(client, t)) + s.AddResourceTemplate(getRepositoryResourcePrContent(client, t)) // Add GitHub tools - Issues s.AddTool(getIssue(client, t)) From b4e67723811058396fb3004bf96e87b4a06b488f Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Tue, 1 Apr 2025 23:16:42 -0700 Subject: [PATCH 07/11] fix test for ubuntu runner --- pkg/github/repository_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index a0419744f..52ef20dda 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -35,7 +35,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { expectedDirContent := []mcp.TextResourceContents{ { URI: "https://github.com/owner/repo/blob/main/README.md", - MIMEType: "", + MIMEType: "text/markdown; charset=utf-8", Text: "README.md", }, { From db7a180177718aa1b71216a0facb13b009f9e2e9 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Tue, 1 Apr 2025 23:19:36 -0700 Subject: [PATCH 08/11] remove it for now --- pkg/github/repository_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 52ef20dda..a0419744f 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -35,7 +35,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { expectedDirContent := []mcp.TextResourceContents{ { URI: "https://github.com/owner/repo/blob/main/README.md", - MIMEType: "text/markdown; charset=utf-8", + MIMEType: "", Text: "README.md", }, { From 02ebdc75742eceb2e444307a1d97876310211959 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Wed, 2 Apr 2025 07:20:11 -0700 Subject: [PATCH 09/11] make required args explicit instead of panic --- pkg/github/repository_resource.go | 24 ++++++++++++++++++------ pkg/github/repository_resource_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 5c87d59b2..e4e5c562f 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -3,6 +3,7 @@ package github import ( "context" "encoding/base64" + "errors" "mime" "path/filepath" "strings" @@ -60,30 +61,41 @@ func getRepositoryResourcePrContent(client *github.Client, t translations.Transl func repositoryResourceContentsHandler(client *github.Client) 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 elemenent + // 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] - owner := request.Params.Arguments["owner"].([]string)[0] - repo := request.Params.Arguments["repo"].([]string)[0] // path should be a joined list of the path parts path := strings.Join(request.Params.Arguments["path"].([]string), "/") opts := &github.RepositoryContentGetOptions{} sha, ok := request.Params.Arguments["sha"].([]string) - if ok { + if ok && len(sha) > 0 { opts.Ref = sha[0] } branch, ok := request.Params.Arguments["branch"].([]string) - if ok { + if ok && len(branch) > 0 { opts.Ref = "refs/heads/" + branch[0] } tag, ok := request.Params.Arguments["tag"].([]string) - if ok { + if ok && len(tag) > 0 { opts.Ref = "refs/tags/" + tag[0] } prNumber, ok := request.Params.Arguments["pr_number"].([]string) - if ok { + if ok && len(prNumber) > 0 { opts.Ref = "refs/pull/" + prNumber[0] + "/head" } diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index a0419744f..533aa6c99 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -70,6 +70,30 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { expectedResult any expectedErrMsg string }{ + { + name: "missing owner", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposContentsByOwnerByRepoByPath, + mockFileContent, + ), + ), + requestArgs: map[string]any{}, + expectError: true, + }, + { + name: "missing repo", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposContentsByOwnerByRepoByPath, + mockFileContent, + ), + ), + requestArgs: map[string]any{ + "owner": []string{"owner"}, + }, + expectError: true, + }, { name: "successful file content fetch", mockedClient: mock.NewMockedHTTPClient( From ad5822019b4a5c470633f70072d9652f317f12e5 Mon Sep 17 00:00:00 2001 From: Ariel Deitcher Date: Wed, 2 Apr 2025 08:31:02 -0700 Subject: [PATCH 10/11] more tests and cleanup --- pkg/github/repository_resource.go | 8 ++++-- pkg/github/repository_resource_test.go | 35 ++++++++++++++++---------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index e4e5c562f..c909e9c3b 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -76,7 +76,11 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C repo := r[0] // path should be a joined list of the path parts - path := strings.Join(request.Params.Arguments["path"].([]string), "/") + path := "" + p, ok := request.Params.Arguments["path"].([]string) + if ok { + path = strings.Join(p, "/") + } opts := &github.RepositoryContentGetOptions{} @@ -150,6 +154,6 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C } } - return nil, nil + return nil, errors.New("no repository resource content found") } } diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 533aa6c99..702a488ca 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -10,7 +10,6 @@ import ( "github.com/google/go-github/v69/github" "github.com/mark3labs/mcp-go/mcp" "github.com/migueleliasweb/go-github-mock/src/mock" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -66,7 +65,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { name string mockedClient *http.Client requestArgs map[string]any - expectError bool + expectError string expectedResult any expectedErrMsg string }{ @@ -79,7 +78,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { ), ), requestArgs: map[string]any{}, - expectError: true, + expectError: "owner is required", }, { name: "missing repo", @@ -92,7 +91,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { requestArgs: map[string]any{ "owner": []string{"owner"}, }, - expectError: true, + expectError: "repo is required", }, { name: "successful file content fetch", @@ -108,7 +107,6 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { "path": []string{"README.md"}, "branch": []string{"main"}, }, - expectError: false, expectedResult: expectedFileContent, }, { @@ -124,11 +122,25 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { "repo": []string{"repo"}, "path": []string{"src"}, }, - expectError: false, expectedResult: expectedDirContent, }, { - name: "empty content fetch", + name: "no data", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposContentsByOwnerByRepoByPath, + ), + ), + requestArgs: map[string]any{ + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"src"}, + }, + expectedResult: nil, + expectError: "no repository resource content found", + }, + { + name: "empty data", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposContentsByOwnerByRepoByPath, @@ -140,7 +152,6 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { "repo": []string{"repo"}, "path": []string{"src"}, }, - expectError: false, expectedResult: nil, }, { @@ -160,8 +171,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { "path": []string{"nonexistent.md"}, "branch": []string{"main"}, }, - expectError: true, - expectedErrMsg: "404 Not Found", + expectError: "404 Not Found", }, } @@ -181,9 +191,8 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { resp, err := handler(context.TODO(), request) - if tc.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErrMsg) + if tc.expectError != "" { + require.ErrorContains(t, err, tc.expectedErrMsg) return } From ede9f22fb02653744653de3a50abd36047747d18 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Apr 2025 23:28:25 +0200 Subject: [PATCH 11/11] chore: use raw repo resources (#70) * use raw repo URIs for resources * fetch repository content from raw urls * ensure no error in test write * Update pkg/github/repository_resource.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * use appropriate file name for text file test --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/github/repository_resource.go | 54 +++++++++++++-- pkg/github/repository_resource_test.go | 92 +++++++++++++++++++++----- 2 files changed, 122 insertions(+), 24 deletions(-) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index c909e9c3b..9fa74c3c6 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -4,7 +4,10 @@ import ( "context" "encoding/base64" "errors" + "fmt" + "io" "mime" + "net/http" "path/filepath" "strings" @@ -113,7 +116,12 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C for _, entry := range directoryContent { mimeType := "text/directory" if entry.GetType() == "file" { - mimeType = mime.TypeByExtension(filepath.Ext(entry.GetName())) + // this is system dependent, and a best guess + ext := filepath.Ext(entry.GetName()) + mimeType = mime.TypeByExtension(ext) + if ext == ".md" { + mimeType = "text/markdown" + } } resources = append(resources, mcp.TextResourceContents{ URI: entry.GetHTMLURL(), @@ -127,28 +135,62 @@ func repositoryResourceContentsHandler(client *github.Client) func(ctx context.C } if fileContent != nil { if fileContent.Content != nil { - decodedContent, err := fileContent.GetContent() + // download the file content from fileContent.GetDownloadURL() and use the content-type header to determine the MIME type + // and return the content as a blob unless it is a text file, where you can return the content as text + req, err := http.NewRequest("GET", fileContent.GetDownloadURL(), nil) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create request: %w", err) } - mimeType := mime.TypeByExtension(filepath.Ext(fileContent.GetName())) + resp, err := client.Client().Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + 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 file content: %s", string(body)) + } + + ext := filepath.Ext(fileContent.GetName()) + mimeType := resp.Header.Get("Content-Type") + if ext == ".md" { + mimeType = "text/markdown" + } else if mimeType == "" { + // backstop to the file extension if the content type is not set + mimeType = mime.TypeByExtension(filepath.Ext(fileContent.GetName())) + } + // if the content is a string, return it as text if strings.HasPrefix(mimeType, "text") { + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to parse the response body: %w", err) + } + return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: request.Params.URI, MIMEType: mimeType, - Text: decodedContent, + Text: string(content), }, }, nil } + // otherwise, read the content and encode it as base64 + decodedContent, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to parse the response body: %w", err) + } return []mcp.ResourceContents{ mcp.BlobResourceContents{ URI: request.Params.URI, MIMEType: mimeType, - Blob: base64.StdEncoding.EncodeToString([]byte(decodedContent)), // Encode content as Base64 + Blob: base64.StdEncoding.EncodeToString(decodedContent), // Encode content as Base64 }, }, nil } diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index 702a488ca..0a5b0b0f0 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -2,7 +2,6 @@ package github import ( "context" - "encoding/base64" "net/http" "testing" @@ -13,28 +12,35 @@ import ( "github.com/stretchr/testify/require" ) +var GetRawReposContentsByOwnerByRepoByPath mock.EndpointPattern = mock.EndpointPattern{ + Pattern: "/{owner}/{repo}/main/{path:.+}", + Method: "GET", +} + func Test_repositoryResourceContentsHandler(t *testing.T) { mockDirContent := []*github.RepositoryContent{ { - Type: github.Ptr("file"), - Name: github.Ptr("README.md"), - Path: github.Ptr("README.md"), - SHA: github.Ptr("abc123"), - Size: github.Ptr(42), - HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), + Type: github.Ptr("file"), + Name: github.Ptr("README.md"), + Path: github.Ptr("README.md"), + SHA: github.Ptr("abc123"), + Size: github.Ptr(42), + HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), + DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"), }, { - Type: github.Ptr("dir"), - Name: github.Ptr("src"), - Path: github.Ptr("src"), - SHA: github.Ptr("def456"), - HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"), + Type: github.Ptr("dir"), + Name: github.Ptr("src"), + Path: github.Ptr("src"), + SHA: github.Ptr("def456"), + HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"), + DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/src"), }, } expectedDirContent := []mcp.TextResourceContents{ { URI: "https://github.com/owner/repo/blob/main/README.md", - MIMEType: "", + MIMEType: "text/markdown", Text: "README.md", }, { @@ -44,20 +50,41 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { }, } - mockFileContent := &github.RepositoryContent{ + mockTextContent := &github.RepositoryContent{ Type: github.Ptr("file"), Name: github.Ptr("README.md"), Path: github.Ptr("README.md"), - Content: github.Ptr("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku"), // Base64 encoded "# Test Repository\n\nThis is a test repository." + Content: github.Ptr("# Test Repository\n\nThis is a test repository."), SHA: github.Ptr("abc123"), Size: github.Ptr(42), HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"), } + mockFileContent := &github.RepositoryContent{ + Type: github.Ptr("file"), + Name: github.Ptr("data.png"), + Path: github.Ptr("data.png"), + Content: github.Ptr("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku"), // Base64 encoded "# Test Repository\n\nThis is a test repository." + SHA: github.Ptr("abc123"), + Size: github.Ptr(42), + HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/data.png"), + DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/data.png"), + } + expectedFileContent := []mcp.BlobResourceContents{ { - Blob: base64.StdEncoding.EncodeToString([]byte("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku")), + Blob: "IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku", + MIMEType: "image/png", + URI: "", + }, + } + + expectedTextContent := []mcp.TextResourceContents{ + { + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + URI: "", }, } @@ -94,21 +121,50 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { expectError: "repo is required", }, { - name: "successful file content fetch", + name: "successful blob content fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatch( mock.GetReposContentsByOwnerByRepoByPath, mockFileContent, ), + mock.WithRequestMatchHandler( + GetRawReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "image/png") + // as this is given as a png, it will return the content as a blob + _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository.")) + require.NoError(t, err) + }), + ), ), requestArgs: map[string]any{ "owner": []string{"owner"}, "repo": []string{"repo"}, - "path": []string{"README.md"}, + "path": []string{"data.png"}, "branch": []string{"main"}, }, expectedResult: expectedFileContent, }, + { + name: "successful text content fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposContentsByOwnerByRepoByPath, + mockTextContent, + ), + mock.WithRequestMatch( + GetRawReposContentsByOwnerByRepoByPath, + []byte("# Test Repository\n\nThis is a test repository."), + ), + ), + requestArgs: map[string]any{ + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"README.md"}, + "branch": []string{"main"}, + }, + expectedResult: expectedTextContent, + }, { name: "successful directory content fetch", mockedClient: mock.NewMockedHTTPClient(