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( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy