Skip to content
Draft
5 changes: 3 additions & 2 deletions pkg/github/repository_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,14 @@ func RepositoryResourceContentsHandler(resourceURITemplate *uritemplate.Template
}

resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
if err != nil {
return nil, fmt.Errorf("failed to get raw content: %w", err)
}
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")
Expand Down
60 changes: 60 additions & 0 deletions pkg/github/repository_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@ package github

import (
"context"
"errors"
"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/v79/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/require"
)

// errorTransport is a http.RoundTripper that always returns an error.
type errorTransport struct {
err error
}

func (t *errorTransport) RoundTrip(*http.Request) (*http.Response, error) {
return nil, t.err
}

type resourceResponseType int

const (
Expand Down Expand Up @@ -272,3 +283,52 @@ func Test_repositoryResourceContents(t *testing.T) {
})
}
}

// Test_repositoryResourceContentsHandler_NetworkError tests that a network error
// during raw content fetch does not cause a panic (nil response body dereference).
func Test_repositoryResourceContentsHandler_NetworkError(t *testing.T) {
base, _ := url.Parse("https://raw.example.com/")
networkErr := errors.New("network error: connection refused")

httpClient := &http.Client{Transport: &errorTransport{err: networkErr}}
client := github.NewClient(httpClient)
mockRawClient := raw.NewClient(client, base)

deps := BaseDeps{
Client: client,
RawClient: mockRawClient,
}
ctx := ContextWithDeps(context.Background(), deps)
handler := RepositoryResourceContentsHandler(repositoryResourceContentURITemplate)

request := &mcp.ReadResourceRequest{
Params: &mcp.ReadResourceParams{
URI: "repo://owner/repo/contents/README.md",
},
}

// This should not panic, even though the HTTP client returns an error
resp, err := handler(ctx, request)
require.Error(t, err)
require.Nil(t, resp)
require.ErrorContains(t, err, "failed to get raw content")
}

func Test_GetRepositoryResourceContent(t *testing.T) {
tmpl := GetRepositoryResourceContent(translations.NullTranslationHelper)
require.Equal(t, "repo://{owner}/{repo}/contents{/path*}", tmpl.Template.URITemplate)
}

func Test_GetRepositoryResourceBranchContent(t *testing.T) {
tmpl := GetRepositoryResourceBranchContent(translations.NullTranslationHelper)
require.Equal(t, "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", tmpl.Template.URITemplate)
}
func Test_GetRepositoryResourceCommitContent(t *testing.T) {
tmpl := GetRepositoryResourceCommitContent(translations.NullTranslationHelper)
require.Equal(t, "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", tmpl.Template.URITemplate)
}

func Test_GetRepositoryResourceTagContent(t *testing.T) {
tmpl := GetRepositoryResourceTagContent(translations.NullTranslationHelper)
require.Equal(t, "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", tmpl.Template.URITemplate)
}