Content-Length: 53293 | pFad | http://github.com/github/github-mcp-server/pull/69.patch
thub.com
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(
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/github/github-mcp-server/pull/69.patch
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy