From fcdbf3ea28e4cdea7cbc594ef27bd12319190476 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Thu, 12 Jun 2025 02:48:00 +0200 Subject: [PATCH] fix: use better raw file handling and return resources --- internal/ghmcp/server.go | 29 +- pkg/github/helper_test.go | 30 ++ pkg/github/repositories.go | 108 ++++++-- pkg/github/repositories_test.go | 114 +++++--- pkg/github/repository_resource.go | 173 ++++++------ pkg/github/repository_resource_test.go | 257 +++++++++--------- pkg/github/server_test.go | 7 + pkg/github/tools.go | 15 +- pkg/raw/raw.go | 69 +++++ pkg/raw/raw_mock.go | 20 ++ pkg/raw/raw_test.go | 150 ++++++++++ third-party-licenses.darwin.md | 4 + third-party-licenses.linux.md | 4 + third-party-licenses.windows.md | 4 + .../google/go-github/v71/github/LICENSE | 27 ++ third-party/github.com/gorilla/mux/LICENSE | 27 ++ .../go-github-mock/src/mock/LICENSE | 21 ++ third-party/golang.org/x/time/rate/LICENSE | 27 ++ 18 files changed, 800 insertions(+), 286 deletions(-) create mode 100644 pkg/raw/raw.go create mode 100644 pkg/raw/raw_mock.go create mode 100644 pkg/raw/raw_test.go create mode 100644 third-party/github.com/google/go-github/v71/github/LICENSE create mode 100644 third-party/github.com/gorilla/mux/LICENSE create mode 100644 third-party/github.com/migueleliasweb/go-github-mock/src/mock/LICENSE create mode 100644 third-party/golang.org/x/time/rate/LICENSE diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 9a9c73926..ca38e76b3 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -14,6 +14,7 @@ import ( "github.com/github/github-mcp-server/pkg/github" mcplog "github.com/github/github-mcp-server/pkg/log" + "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" gogithub "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" @@ -112,8 +113,16 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { return gqlClient, nil // closing over client } + getRawClient := func(ctx context.Context) (*raw.Client, error) { + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + return raw.NewClient(client, apiHost.rawURL), nil // closing over client + } + // Create default toolsets - tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, cfg.Translator) + tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, getRawClient, cfg.Translator) err = tsg.EnableToolsets(enabledToolsets) if err != nil { @@ -237,6 +246,7 @@ type apiHost struct { baseRESTURL *url.URL graphqlURL *url.URL uploadURL *url.URL + rawURL *url.URL } func newDotcomHost() (apiHost, error) { @@ -255,10 +265,16 @@ func newDotcomHost() (apiHost, error) { return apiHost{}, fmt.Errorf("failed to parse dotcom Upload URL: %w", err) } + rawURL, err := url.Parse("https://raw.githubusercontent.com/") + if err != nil { + return apiHost{}, fmt.Errorf("failed to parse dotcom Raw URL: %w", err) + } + return apiHost{ baseRESTURL: baseRestURL, graphqlURL: gqlURL, uploadURL: uploadURL, + rawURL: rawURL, }, nil } @@ -288,10 +304,16 @@ func newGHECHost(hostname string) (apiHost, error) { return apiHost{}, fmt.Errorf("failed to parse GHEC Upload URL: %w", err) } + rawURL, err := url.Parse(fmt.Sprintf("https://raw.%s/", u.Hostname())) + if err != nil { + return apiHost{}, fmt.Errorf("failed to parse GHEC Raw URL: %w", err) + } + return apiHost{ baseRESTURL: restURL, graphqlURL: gqlURL, uploadURL: uploadURL, + rawURL: rawURL, }, nil } @@ -315,11 +337,16 @@ func newGHESHost(hostname string) (apiHost, error) { if err != nil { return apiHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err) } + rawURL, err := url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname())) + if err != nil { + return apiHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err) + } return apiHost{ baseRESTURL: restURL, graphqlURL: gqlURL, uploadURL: uploadURL, + rawURL: rawURL, }, nil } diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go index 4b9a243de..bc1ae412f 100644 --- a/pkg/github/helper_test.go +++ b/pkg/github/helper_test.go @@ -132,6 +132,36 @@ func getTextResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent { return textContent } +func getErrorResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent { + res := getTextResult(t, result) + require.True(t, result.IsError, "expected tool call result to be an error") + return res +} + +// getTextResourceResult is a helper function that returns a text result from a tool call. +func getTextResourceResult(t *testing.T, result *mcp.CallToolResult) mcp.TextResourceContents { + t.Helper() + assert.NotNil(t, result) + require.Len(t, result.Content, 2) + content := result.Content[1] + require.IsType(t, mcp.EmbeddedResource{}, content) + resource := content.(mcp.EmbeddedResource) + require.IsType(t, mcp.TextResourceContents{}, resource.Resource) + return resource.Resource.(mcp.TextResourceContents) +} + +// getBlobResourceResult is a helper function that returns a blob result from a tool call. +func getBlobResourceResult(t *testing.T, result *mcp.CallToolResult) mcp.BlobResourceContents { + t.Helper() + assert.NotNil(t, result) + require.Len(t, result.Content, 2) + content := result.Content[1] + require.IsType(t, mcp.EmbeddedResource{}, content) + resource := content.(mcp.EmbeddedResource) + require.IsType(t, mcp.BlobResourceContents{}, resource.Resource) + return resource.Resource.(mcp.BlobResourceContents) +} + func TestOptionalParamOK(t *testing.T) { tests := []struct { name string diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 093e5fdcd..3475167b1 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -2,11 +2,15 @@ package github import ( "context" + "encoding/base64" "encoding/json" "fmt" "io" "net/http" + "net/url" + "strings" + "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" @@ -409,7 +413,7 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun } // GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository. -func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_file_contents", mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ @@ -426,7 +430,7 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc ), mcp.WithString("path", mcp.Required(), - mcp.Description("Path to file/directory"), + mcp.Description("Path to file/directory (directories must end with a slash '/')"), ), mcp.WithString("branch", mcp.Description("Branch to get contents from"), @@ -450,38 +454,92 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc return mcp.NewToolResultError(err.Error()), nil } - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + // If the path is (most likely) not to be a directory, we will first try to get the raw content from the GitHub raw content API. + if path != "" && !strings.HasSuffix(path, "/") { + rawOpts := &raw.RawContentOpts{} + if branch != "" { + rawOpts.Ref = "refs/heads/" + branch + } + rawClient, err := getRawClient(ctx) + if err != nil { + return mcp.NewToolResultError("failed to get GitHub raw content client"), nil + } + resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts) + if err != nil { + return mcp.NewToolResultError("failed to get raw repository content"), nil + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + // If the raw content is not found, we will fall back to the GitHub API (in case it is a directory) + } else { + // If the raw content is found, return it directly + body, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError("failed to read response body"), nil + } + contentType := resp.Header.Get("Content-Type") + + var resourceURI string + if branch == "" { + // do a safe url join + resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path) + if err != nil { + return nil, fmt.Errorf("failed to create resource URI: %w", err) + } + } else { + resourceURI, err = url.JoinPath("repo://", owner, repo, "refs", "heads", branch, "contents", path) + if err != nil { + return nil, fmt.Errorf("failed to create resource URI: %w", err) + } + } + if strings.HasPrefix(contentType, "application") || strings.HasPrefix(contentType, "text") { + return mcp.NewToolResultResource("successfully downloaded text file", mcp.TextResourceContents{ + URI: resourceURI, + Text: string(body), + MIMEType: contentType, + }), nil + } + + return mcp.NewToolResultResource("successfully downloaded binary file", mcp.BlobResourceContents{ + URI: resourceURI, + Blob: base64.StdEncoding.EncodeToString(body), + MIMEType: contentType, + }), nil + + } } - opts := &github.RepositoryContentGetOptions{Ref: branch} - fileContent, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) + + client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get file contents: %w", err) + return mcp.NewToolResultError("failed to get GitHub client"), nil } - defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != 200 { - body, err := io.ReadAll(resp.Body) + if strings.HasSuffix(path, "/") { + opts := &github.RepositoryContentGetOptions{Ref: branch} + _, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return mcp.NewToolResultError("failed to get file contents"), nil } - return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil - } + defer func() { _ = resp.Body.Close() }() - var result interface{} - if fileContent != nil { - result = fileContent - } else { - result = dirContent - } + if resp.StatusCode != 200 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError("failed to read response body"), nil + } + return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil + } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + r, err := json.Marshal(dirContent) + if err != nil { + return mcp.NewToolResultError("failed to marshal response"), nil + } + return mcp.NewToolResultText(string(r)), nil } - - return mcp.NewToolResultText(string(r)), nil + return mcp.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil } } diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index f7924b2f9..c2585341e 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -2,13 +2,17 @@ package github import ( "context" + "encoding/base64" "encoding/json" "net/http" + "net/url" "testing" "time" + "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/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" @@ -17,7 +21,8 @@ import ( func Test_GetFileContents(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetFileContents(stubGetClientFn(mockClient), translations.NullTranslationHelper) + mockRawClient := raw.NewClient(mockClient, &url.URL{Scheme: "https", Host: "raw.githubusercontent.com", Path: "/"}) + tool, _ := GetFileContents(stubGetClientFn(mockClient), stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) assert.Equal(t, "get_file_contents", tool.Name) assert.NotEmpty(t, tool.Description) @@ -27,17 +32,8 @@ func Test_GetFileContents(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "branch") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "path"}) - // Setup mock file content for success case - 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"), - } + // Mock response for raw content + mockRawContent := []byte("# Test Repository\n\nThis is a test repository.") // Setup mock directory content for success case mockDirContent := []*github.RepositoryContent{ @@ -65,17 +61,17 @@ func Test_GetFileContents(t *testing.T) { expectError bool expectedResult interface{} expectedErrMsg string + expectStatus int }{ { - name: "successful file content fetch", + name: "successful text content fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( - mock.GetReposContentsByOwnerByRepoByPath, - expectQueryParams(t, map[string]string{ - "ref": "main", - }).andThen( - mockResponse(t, http.StatusOK, mockFileContent), - ), + raw.GetRawReposContentsByOwnerByRepoByBranchByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/markdown") + _, _ = w.Write(mockRawContent) + }), ), ), requestArgs: map[string]interface{}{ @@ -84,8 +80,36 @@ func Test_GetFileContents(t *testing.T) { "path": "README.md", "branch": "main", }, - expectError: false, - expectedResult: mockFileContent, + expectError: false, + expectedResult: mcp.TextResourceContents{ + URI: "repo://owner/repo/refs/heads/main/contents/README.md", + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + }, + }, + { + name: "successful file blob content fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoByBranchByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "image/png") + _, _ = w.Write(mockRawContent) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "path": "test.png", + "branch": "main", + }, + expectError: false, + expectedResult: mcp.BlobResourceContents{ + URI: "repo://owner/repo/refs/heads/main/contents/test.png", + Blob: base64.StdEncoding.EncodeToString(mockRawContent), + MIMEType: "image/png", + }, }, { name: "successful directory content fetch", @@ -96,11 +120,19 @@ func Test_GetFileContents(t *testing.T) { mockResponse(t, http.StatusOK, mockDirContent), ), ), + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoByPath, + expectQueryParams(t, map[string]string{ + "branch": "main", + }).andThen( + mockResponse(t, http.StatusNotFound, nil), + ), + ), ), requestArgs: map[string]interface{}{ "owner": "owner", "repo": "repo", - "path": "src", + "path": "src/", }, expectError: false, expectedResult: mockDirContent, @@ -115,6 +147,13 @@ func Test_GetFileContents(t *testing.T) { _, _ = w.Write([]byte(`{"message": "Not Found"}`)) }), ), + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), ), requestArgs: map[string]interface{}{ "owner": "owner", @@ -122,8 +161,8 @@ func Test_GetFileContents(t *testing.T) { "path": "nonexistent.md", "branch": "main", }, - expectError: true, - expectedErrMsg: "failed to get file contents", + expectError: false, + expectedResult: mcp.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), }, } @@ -131,7 +170,8 @@ func Test_GetFileContents(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetFileContents(stubGetClientFn(client), translations.NullTranslationHelper) + mockRawClient := raw.NewClient(client, &url.URL{Scheme: "https", Host: "raw.example.com", Path: "/"}) + _, handler := GetFileContents(stubGetClientFn(client), stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -147,20 +187,17 @@ func Test_GetFileContents(t *testing.T) { } require.NoError(t, err) - - // Parse the result and get the text content if no error - textContent := getTextResult(t, result) - - // Verify based on expected type + // Use the correct result helper based on the expected type switch expected := tc.expectedResult.(type) { - case *github.RepositoryContent: - var returnedContent github.RepositoryContent - err = json.Unmarshal([]byte(textContent.Text), &returnedContent) - require.NoError(t, err) - assert.Equal(t, *expected.Name, *returnedContent.Name) - assert.Equal(t, *expected.Path, *returnedContent.Path) - assert.Equal(t, *expected.Type, *returnedContent.Type) + case mcp.TextResourceContents: + textResource := getTextResourceResult(t, result) + assert.Equal(t, expected, textResource) + case mcp.BlobResourceContents: + blobResource := getBlobResourceResult(t, result) + assert.Equal(t, expected, blobResource) case []*github.RepositoryContent: + // Directory content fetch returns a text result (JSON array) + textContent := getTextResult(t, result) var returnedContents []*github.RepositoryContent err = json.Unmarshal([]byte(textContent.Text), &returnedContents) require.NoError(t, err) @@ -170,6 +207,9 @@ func Test_GetFileContents(t *testing.T) { assert.Equal(t, *expected[i].Path, *content.Path) assert.Equal(t, *expected[i].Type, *content.Type) } + case mcp.TextContent: + textContent := getErrorResult(t, result) + require.Equal(t, textContent, expected) } }) } diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 7e1ce51cc..fd2a04f89 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -9,8 +9,10 @@ import ( "mime" "net/http" "path/filepath" + "strconv" "strings" + "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" @@ -18,52 +20,52 @@ import ( ) // GetRepositoryResourceContent defines the resource template and handler for getting repository content. -func GetRepositoryResourceContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), ), - RepositoryResourceContentsHandler(getClient) + RepositoryResourceContentsHandler(getClient, getRawClient) } // GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch. -func GetRepositoryResourceBranchContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, 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"), ), - RepositoryResourceContentsHandler(getClient) + RepositoryResourceContentsHandler(getClient, getRawClient) } // GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit. -func GetRepositoryResourceCommitContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, 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"), ), - RepositoryResourceContentsHandler(getClient) + RepositoryResourceContentsHandler(getClient, getRawClient) } // GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag. -func GetRepositoryResourceTagContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, 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"), ), - RepositoryResourceContentsHandler(getClient) + RepositoryResourceContentsHandler(getClient, getRawClient) } // GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request. -func GetRepositoryResourcePrContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourcePrContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), ), - RepositoryResourceContentsHandler(getClient) + RepositoryResourceContentsHandler(getClient, getRawClient) } // RepositoryResourceContentsHandler returns a handler function for repository content requests. -func RepositoryResourceContentsHandler(getClient GetClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { +func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.GetRawClientFn) 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 element // https://github.com/mark3labs/mcp-go/pull/54 @@ -87,121 +89,104 @@ func RepositoryResourceContentsHandler(getClient GetClientFn) func(ctx context.C } opts := &github.RepositoryContentGetOptions{} + rawOpts := &raw.RawContentOpts{} sha, ok := request.Params.Arguments["sha"].([]string) if ok && len(sha) > 0 { opts.Ref = sha[0] + rawOpts.SHA = sha[0] } branch, ok := request.Params.Arguments["branch"].([]string) if ok && len(branch) > 0 { opts.Ref = "refs/heads/" + branch[0] + rawOpts.Ref = "refs/heads/" + branch[0] } tag, ok := request.Params.Arguments["tag"].([]string) if ok && len(tag) > 0 { opts.Ref = "refs/tags/" + tag[0] + rawOpts.Ref = "refs/tags/" + tag[0] } prNumber, ok := request.Params.Arguments["prNumber"].([]string) if ok && len(prNumber) > 0 { - opts.Ref = "refs/pull/" + prNumber[0] + "/head" + // fetch the PR from the API to get the latest commit and use SHA + githubClient, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + prNum, err := strconv.Atoi(prNumber[0]) + if err != nil { + return nil, fmt.Errorf("invalid pull request number: %w", err) + } + pr, _, err := githubClient.PullRequests.Get(ctx, owner, repo, prNum) + if err != nil { + return nil, fmt.Errorf("failed to get pull request: %w", err) + } + sha := pr.GetHead().GetSHA() + rawOpts.SHA = sha + opts.Ref = sha } - - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + // if it's a directory + if path == "" || strings.HasSuffix(path, "/") { + return nil, fmt.Errorf("directories are not supported: %s", path) } - fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) + rawClient, err := getRawClient(ctx) + if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get GitHub raw content client: %w", err) } - if directoryContent != nil { - var resources []mcp.ResourceContents - for _, entry := range directoryContent { - mimeType := "text/directory" - if entry.GetType() == "file" { - // 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(), - MIMEType: mimeType, - Text: entry.GetName(), - }) - + resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts) + defer func() { + _ = resp.Body.Close() + }() + // If the raw content is not found, we will fall back to the GitHub API (in case it is a directory) + switch { + case err != nil: + return nil, fmt.Errorf("failed to get raw content: %w", err) + case resp.StatusCode == http.StatusOK: + ext := filepath.Ext(path) + mimeType := resp.Header.Get("Content-Type") + if ext == ".md" { + mimeType = "text/markdown" + } else if mimeType == "" { + mimeType = mime.TypeByExtension(ext) } - return resources, nil - } - if fileContent != nil { - if fileContent.Content != nil { - // 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, fmt.Errorf("failed to create request: %w", err) - } - - 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: 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) - } + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read file content: %w", err) + } + switch { + case strings.HasPrefix(mimeType, "text"), strings.HasPrefix(mimeType, "application"): + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: request.Params.URI, + MIMEType: mimeType, + Text: string(content), + }, + }, nil + default: return []mcp.ResourceContents{ mcp.BlobResourceContents{ URI: request.Params.URI, MIMEType: mimeType, - Blob: base64.StdEncoding.EncodeToString(decodedContent), // Encode content as Base64 + Blob: base64.StdEncoding.EncodeToString(content), }, }, nil } + case resp.StatusCode != http.StatusNotFound: + // If we got a response but it is not 200 OK, we return an error + 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 raw content: %s", string(body)) + default: + // This should be unreachable because GetContents should return an error if neither file nor directory content is found. + return nil, errors.New("404 Not Found") } - - // This should be unreachable because GetContents should return an error if neither file nor directory content is found. - 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 a99edb5cf..0e9f018e7 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -3,8 +3,10 @@ package github import ( "context" "net/http" + "net/url" "testing" + "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" @@ -12,82 +14,8 @@ 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"), - 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"), - 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: "text/markdown", - Text: "README.md", - }, - { - URI: "https://github.com/owner/repo/tree/main/src", - MIMEType: "text/directory", - Text: "src", - }, - } - - mockTextContent := &github.RepositoryContent{ - Type: github.Ptr("file"), - Name: github.Ptr("README.md"), - Path: github.Ptr("README.md"), - 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: "IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku", - MIMEType: "image/png", - URI: "", - }, - } - - expectedTextContent := []mcp.TextResourceContents{ - { - Text: "# Test Repository\n\nThis is a test repository.", - MIMEType: "text/markdown", - URI: "", - }, - } - + base, _ := url.Parse("https://raw.example.com/") tests := []struct { name string mockedClient *http.Client @@ -98,9 +26,14 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { { name: "missing owner", mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposContentsByOwnerByRepoByPath, - mockFileContent, + mock.WithRequestMatchHandler( + raw.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{}, @@ -109,9 +42,14 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { { name: "missing repo", mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposContentsByOwnerByRepoByPath, - mockFileContent, + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoByBranchByPath, + 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{ @@ -122,38 +60,59 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { { name: "successful blob content fetch", mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposContentsByOwnerByRepoByPath, - mockFileContent, - ), mock.WithRequestMatchHandler( - GetRawReposContentsByOwnerByRepoByPath, + raw.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{"data.png"}, - "branch": []string{"main"}, + "owner": []string{"owner"}, + "repo": []string{"repo"}, + "path": []string{"data.png"}, }, - expectedResult: expectedFileContent, + expectedResult: []mcp.BlobResourceContents{{ + Blob: "IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku", + MIMEType: "image/png", + URI: "", + }}, }, { - name: "successful text content fetch", + name: "successful text content fetch (HEAD)", mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposContentsByOwnerByRepoByPath, - mockTextContent, + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/markdown") + _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository.")) + require.NoError(t, err) + }), ), - 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"}, + }, + expectedResult: []mcp.TextResourceContents{{ + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + URI: "", + }}, + }, + { + name: "successful text content fetch (branch)", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoByBranchByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/markdown") + _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository.")) + require.NoError(t, err) + }), ), ), requestArgs: map[string]any{ @@ -162,37 +121,91 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { "path": []string{"README.md"}, "branch": []string{"main"}, }, - expectedResult: expectedTextContent, + expectedResult: []mcp.TextResourceContents{{ + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + URI: "", + }}, }, { - name: "successful directory content fetch", + name: "successful text content fetch (tag)", mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposContentsByOwnerByRepoByPath, - mockDirContent, + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoByTagByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/markdown") + _, 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{"src"}, + "path": []string{"README.md"}, + "tag": []string{"v1.0.0"}, }, - expectedResult: expectedDirContent, + expectedResult: []mcp.TextResourceContents{{ + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + URI: "", + }}, }, { - name: "empty data", + name: "successful text content fetch (sha)", mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposContentsByOwnerByRepoByPath, - []*github.RepositoryContent{}, + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoBySHAByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/markdown") + _, 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{"src"}, + "path": []string{"README.md"}, + "sha": []string{"abc123"}, + }, + expectedResult: []mcp.TextResourceContents{{ + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + URI: "", + }}, + }, + { + name: "successful text content fetch (pr)", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposPullsByOwnerByRepoByPullNumber, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`{"head": {"sha": "abc123"}}`)) + require.NoError(t, err) + }), + ), + mock.WithRequestMatchHandler( + raw.GetRawReposContentsByOwnerByRepoBySHAByPath, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/markdown") + _, 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"}, + "prNumber": []string{"42"}, }, - expectedResult: nil, + expectedResult: []mcp.TextResourceContents{{ + Text: "# Test Repository\n\nThis is a test repository.", + MIMEType: "text/markdown", + URI: "", + }}, }, { name: "content fetch fails", @@ -218,7 +231,8 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - handler := RepositoryResourceContentsHandler((stubGetClientFn(client))) + mockRawClient := raw.NewClient(client, base) + handler := RepositoryResourceContentsHandler((stubGetClientFn(client)), stubGetRawClientFn(mockRawClient)) request := mcp.ReadResourceRequest{ Params: struct { @@ -243,25 +257,24 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { } func Test_GetRepositoryResourceContent(t *testing.T) { - tmpl, _ := GetRepositoryResourceContent(nil, translations.NullTranslationHelper) + mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{}) + tmpl, _ := GetRepositoryResourceContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) require.Equal(t, "repo://{owner}/{repo}/contents{/path*}", tmpl.URITemplate.Raw()) } func Test_GetRepositoryResourceBranchContent(t *testing.T) { - tmpl, _ := GetRepositoryResourceBranchContent(nil, translations.NullTranslationHelper) + mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{}) + tmpl, _ := GetRepositoryResourceBranchContent(nil, stubGetRawClientFn(mockRawClient), 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) + mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{}) + tmpl, _ := GetRepositoryResourceCommitContent(nil, stubGetRawClientFn(mockRawClient), 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) + mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{}) + tmpl, _ := GetRepositoryResourceTagContent(nil, stubGetRawClientFn(mockRawClient), 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/{prNumber}/head/contents{/path*}", tmpl.URITemplate.Raw()) -} diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index db0b0b237..3f00d7b24 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -8,6 +8,7 @@ import ( "net/http" "testing" + "github.com/github/github-mcp-server/pkg/raw" "github.com/google/go-github/v72/github" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" @@ -37,6 +38,12 @@ func stubGetGQLClientFn(client *githubv4.Client) GetGQLClientFn { } } +func stubGetRawClientFn(client *raw.Client) raw.GetRawClientFn { + return func(_ context.Context) (*raw.Client, error) { + return client, nil + } +} + func badRequestHandler(msg string) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { structuredErrorResponse := github.ErrorResponse{ diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 0a3e72459..9569c4390 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -3,6 +3,7 @@ package github import ( "context" + "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" @@ -15,7 +16,7 @@ type GetGQLClientFn func(context.Context) (*githubv4.Client, error) var DefaultTools = []string{"all"} -func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) *toolsets.ToolsetGroup { +func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) *toolsets.ToolsetGroup { tsg := toolsets.NewToolsetGroup(readOnly) // Define all available features with their default state (disabled) @@ -23,7 +24,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG repos := toolsets.NewToolset("repos", "GitHub Repository related tools"). AddReadTools( toolsets.NewServerTool(SearchRepositories(getClient, t)), - toolsets.NewServerTool(GetFileContents(getClient, t)), + toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)), toolsets.NewServerTool(ListCommits(getClient, t)), toolsets.NewServerTool(SearchCode(getClient, t)), toolsets.NewServerTool(GetCommit(getClient, t)), @@ -40,11 +41,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(DeleteFile(getClient, t)), ). AddResourceTemplates( - toolsets.NewServerResourceTemplate(GetRepositoryResourceContent(getClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceBranchContent(getClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceCommitContent(getClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceTagContent(getClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourcePrContent(getClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourceContent(getClient, getRawClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourceBranchContent(getClient, getRawClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourceCommitContent(getClient, getRawClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourceTagContent(getClient, getRawClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourcePrContent(getClient, getRawClient, t)), ) issues := toolsets.NewToolset("issues", "GitHub Issues related tools"). AddReadTools( diff --git a/pkg/raw/raw.go b/pkg/raw/raw.go new file mode 100644 index 000000000..d604891b6 --- /dev/null +++ b/pkg/raw/raw.go @@ -0,0 +1,69 @@ +// Package raw provides a client for interacting with the GitHub raw file API +package raw + +import ( + "context" + "net/http" + "net/url" + + gogithub "github.com/google/go-github/v72/github" +) + +// GetRawClientFn is a function type that returns a RawClient instance. +type GetRawClientFn func(context.Context) (*Client, error) + +// Client is a client for interacting with the GitHub raw content API. +type Client struct { + url *url.URL + client *gogithub.Client +} + +// NewClient creates a new instance of the raw API Client with the provided GitHub client and provided URL. +func NewClient(client *gogithub.Client, rawURL *url.URL) *Client { + client = gogithub.NewClient(client.Client()) + client.BaseURL = rawURL + return &Client{client: client, url: rawURL} +} + +func (c *Client) newRequest(method string, urlStr string, body interface{}, opts ...gogithub.RequestOption) (*http.Request, error) { + req, err := c.client.NewRequest(method, urlStr, body, opts...) + return req, err +} + +func (c *Client) refURL(owner, repo, ref, path string) string { + if ref == "" { + return c.url.JoinPath(owner, repo, "HEAD", path).String() + } + return c.url.JoinPath(owner, repo, ref, path).String() +} + +func (c *Client) URLFromOpts(opts *RawContentOpts, owner, repo, path string) string { + if opts == nil { + opts = &RawContentOpts{} + } + if opts.SHA != "" { + return c.commitURL(owner, repo, opts.SHA, path) + } + return c.refURL(owner, repo, opts.Ref, path) +} + +// BlobURL returns the URL for a blob in the raw content API. +func (c *Client) commitURL(owner, repo, sha, path string) string { + return c.url.JoinPath(owner, repo, sha, path).String() +} + +type RawContentOpts struct { + Ref string + SHA string +} + +// GetRawContent fetches the raw content of a file from a GitHub repository. +func (c *Client) GetRawContent(ctx context.Context, owner, repo, path string, opts *RawContentOpts) (*http.Response, error) { + url := c.URLFromOpts(opts, owner, repo, path) + req, err := c.newRequest("GET", url, nil) + if err != nil { + return nil, err + } + + return c.client.Client().Do(req) +} diff --git a/pkg/raw/raw_mock.go b/pkg/raw/raw_mock.go new file mode 100644 index 000000000..30c7759d3 --- /dev/null +++ b/pkg/raw/raw_mock.go @@ -0,0 +1,20 @@ +package raw + +import "github.com/migueleliasweb/go-github-mock/src/mock" + +var GetRawReposContentsByOwnerByRepoByPath mock.EndpointPattern = mock.EndpointPattern{ + Pattern: "/{owner}/{repo}/HEAD/{path:.*}", + Method: "GET", +} +var GetRawReposContentsByOwnerByRepoByBranchByPath mock.EndpointPattern = mock.EndpointPattern{ + Pattern: "/{owner}/{repo}/refs/heads/{branch}/{path:.*}", + Method: "GET", +} +var GetRawReposContentsByOwnerByRepoByTagByPath mock.EndpointPattern = mock.EndpointPattern{ + Pattern: "/{owner}/{repo}/refs/tags/{tag}/{path:.*}", + Method: "GET", +} +var GetRawReposContentsByOwnerByRepoBySHAByPath mock.EndpointPattern = mock.EndpointPattern{ + Pattern: "/{owner}/{repo}/{sha}/{path:.*}", + Method: "GET", +} diff --git a/pkg/raw/raw_test.go b/pkg/raw/raw_test.go new file mode 100644 index 000000000..bb9b23a28 --- /dev/null +++ b/pkg/raw/raw_test.go @@ -0,0 +1,150 @@ +package raw + +import ( + "context" + "net/http" + "net/url" + "testing" + + "github.com/google/go-github/v72/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/require" +) + +func TestGetRawContent(t *testing.T) { + base, _ := url.Parse("https://raw.example.com/") + + tests := []struct { + name string + pattern mock.EndpointPattern + opts *RawContentOpts + owner, repo, path string + statusCode int + contentType string + body string + expectError string + }{ + { + name: "HEAD fetch success", + pattern: GetRawReposContentsByOwnerByRepoByPath, + opts: nil, + owner: "octocat", repo: "hello", path: "README.md", + statusCode: 200, + contentType: "text/plain", + body: "# Test file", + }, + { + name: "branch fetch success", + pattern: GetRawReposContentsByOwnerByRepoByBranchByPath, + opts: &RawContentOpts{Ref: "refs/heads/main"}, + owner: "octocat", repo: "hello", path: "README.md", + statusCode: 200, + contentType: "text/plain", + body: "# Test file", + }, + { + name: "tag fetch success", + pattern: GetRawReposContentsByOwnerByRepoByTagByPath, + opts: &RawContentOpts{Ref: "refs/tags/v1.0.0"}, + owner: "octocat", repo: "hello", path: "README.md", + statusCode: 200, + contentType: "text/plain", + body: "# Test file", + }, + { + name: "sha fetch success", + pattern: GetRawReposContentsByOwnerByRepoBySHAByPath, + opts: &RawContentOpts{SHA: "abc123"}, + owner: "octocat", repo: "hello", path: "README.md", + statusCode: 200, + contentType: "text/plain", + body: "# Test file", + }, + { + name: "not found", + pattern: GetRawReposContentsByOwnerByRepoByPath, + opts: nil, + owner: "octocat", repo: "hello", path: "notfound.txt", + statusCode: 404, + contentType: "application/json", + body: `{"message": "Not Found"}`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockedClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + tc.pattern, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(tc.statusCode) + _, err := w.Write([]byte(tc.body)) + require.NoError(t, err) + }), + ), + ) + ghClient := github.NewClient(mockedClient) + client := NewClient(ghClient, base) + resp, err := client.GetRawContent(context.Background(), tc.owner, tc.repo, tc.path, tc.opts) + defer func() { + _ = resp.Body.Close() + }() + if tc.expectError != "" { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.statusCode, resp.StatusCode) + }) + } +} + +func TestUrlFromOpts(t *testing.T) { + base, _ := url.Parse("https://raw.example.com/") + ghClient := github.NewClient(nil) + client := NewClient(ghClient, base) + + tests := []struct { + name string + opts *RawContentOpts + owner string + repo string + path string + want string + }{ + { + name: "no opts (HEAD)", + opts: nil, + owner: "octocat", repo: "hello", path: "README.md", + want: "https://raw.example.com/octocat/hello/HEAD/README.md", + }, + { + name: "ref branch", + opts: &RawContentOpts{Ref: "refs/heads/main"}, + owner: "octocat", repo: "hello", path: "README.md", + want: "https://raw.example.com/octocat/hello/refs/heads/main/README.md", + }, + { + name: "ref tag", + opts: &RawContentOpts{Ref: "refs/tags/v1.0.0"}, + owner: "octocat", repo: "hello", path: "README.md", + want: "https://raw.example.com/octocat/hello/refs/tags/v1.0.0/README.md", + }, + { + name: "sha", + opts: &RawContentOpts{SHA: "abc123"}, + owner: "octocat", repo: "hello", path: "README.md", + want: "https://raw.example.com/octocat/hello/abc123/README.md", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := client.URLFromOpts(tt.opts, tt.owner, tt.repo, tt.path) + if got != tt.want { + t.Errorf("UrlFromOpts() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 5905f040c..7ba187e1f 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -12,13 +12,16 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE)) - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) + - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) + - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE)) - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE)) - [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md)) - [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE)) - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE)) + - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE)) @@ -36,6 +39,7 @@ Some packages may only be included on certain architectures or operating systems - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/8a7402ab:LICENSE)) - [golang.org/x/sys/unix](https://pkg.go.dev/golang.org/x/sys/unix) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.31.0:LICENSE)) - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.23.0:LICENSE)) + - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE)) - [gopkg.in/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE)) - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index 5905f040c..7ba187e1f 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -12,13 +12,16 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE)) - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) + - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) + - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE)) - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE)) - [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md)) - [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE)) - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE)) + - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE)) @@ -36,6 +39,7 @@ Some packages may only be included on certain architectures or operating systems - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/8a7402ab:LICENSE)) - [golang.org/x/sys/unix](https://pkg.go.dev/golang.org/x/sys/unix) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.31.0:LICENSE)) - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.23.0:LICENSE)) + - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE)) - [gopkg.in/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE)) - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index b5b5c112c..1c8b6c588 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -12,14 +12,17 @@ Some packages may only be included on certain architectures or operating systems - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE)) - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE)) - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) + - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) + - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE)) - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE)) - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE)) - [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md)) - [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE)) - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE)) + - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE)) @@ -37,6 +40,7 @@ Some packages may only be included on certain architectures or operating systems - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/8a7402ab:LICENSE)) - [golang.org/x/sys/windows](https://pkg.go.dev/golang.org/x/sys/windows) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.31.0:LICENSE)) - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.23.0:LICENSE)) + - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE)) - [gopkg.in/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE)) - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)) diff --git a/third-party/github.com/google/go-github/v71/github/LICENSE b/third-party/github.com/google/go-github/v71/github/LICENSE new file mode 100644 index 000000000..28b6486f0 --- /dev/null +++ b/third-party/github.com/google/go-github/v71/github/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 The go-github AUTHORS. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third-party/github.com/gorilla/mux/LICENSE b/third-party/github.com/gorilla/mux/LICENSE new file mode 100644 index 000000000..6903df638 --- /dev/null +++ b/third-party/github.com/gorilla/mux/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third-party/github.com/migueleliasweb/go-github-mock/src/mock/LICENSE b/third-party/github.com/migueleliasweb/go-github-mock/src/mock/LICENSE new file mode 100644 index 000000000..86d42717d --- /dev/null +++ b/third-party/github.com/migueleliasweb/go-github-mock/src/mock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Miguel Elias dos Santos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third-party/golang.org/x/time/rate/LICENSE b/third-party/golang.org/x/time/rate/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/third-party/golang.org/x/time/rate/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.