Skip to content

Commit 01d5727

Browse files
use better raw file handling and return resources
1 parent c423a52 commit 01d5727

File tree

18 files changed

+812
-286
lines changed

18 files changed

+812
-286
lines changed

internal/ghmcp/server.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/github/github-mcp-server/pkg/github"
1616
mcplog "github.com/github/github-mcp-server/pkg/log"
17+
"github.com/github/github-mcp-server/pkg/raw"
1718
"github.com/github/github-mcp-server/pkg/translations"
1819
gogithub "github.com/google/go-github/v72/github"
1920
"github.com/mark3labs/mcp-go/mcp"
@@ -112,8 +113,16 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
112113
return gqlClient, nil // closing over client
113114
}
114115

116+
getRawClient := func(ctx context.Context) (*raw.Client, error) {
117+
client, err := getClient(ctx)
118+
if err != nil {
119+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
120+
}
121+
return raw.NewClient(client, apiHost.rawURL), nil // closing over client
122+
}
123+
115124
// Create default toolsets
116-
tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, cfg.Translator)
125+
tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, getRawClient, cfg.Translator)
117126
err = tsg.EnableToolsets(enabledToolsets)
118127

119128
if err != nil {
@@ -237,6 +246,7 @@ type apiHost struct {
237246
baseRESTURL *url.URL
238247
graphqlURL *url.URL
239248
uploadURL *url.URL
249+
rawURL *url.URL
240250
}
241251

242252
func newDotcomHost() (apiHost, error) {
@@ -255,10 +265,16 @@ func newDotcomHost() (apiHost, error) {
255265
return apiHost{}, fmt.Errorf("failed to parse dotcom Upload URL: %w", err)
256266
}
257267

268+
rawURL, err := url.Parse("https://raw.githubusercontent.com/")
269+
if err != nil {
270+
return apiHost{}, fmt.Errorf("failed to parse dotcom Raw URL: %w", err)
271+
}
272+
258273
return apiHost{
259274
baseRESTURL: baseRestURL,
260275
graphqlURL: gqlURL,
261276
uploadURL: uploadURL,
277+
rawURL: rawURL,
262278
}, nil
263279
}
264280

@@ -288,10 +304,16 @@ func newGHECHost(hostname string) (apiHost, error) {
288304
return apiHost{}, fmt.Errorf("failed to parse GHEC Upload URL: %w", err)
289305
}
290306

307+
rawURL, err := url.Parse(fmt.Sprintf("https://raw.%s/", u.Hostname()))
308+
if err != nil {
309+
return apiHost{}, fmt.Errorf("failed to parse GHEC Raw URL: %w", err)
310+
}
311+
291312
return apiHost{
292313
baseRESTURL: restURL,
293314
graphqlURL: gqlURL,
294315
uploadURL: uploadURL,
316+
rawURL: rawURL,
295317
}, nil
296318
}
297319

@@ -315,11 +337,16 @@ func newGHESHost(hostname string) (apiHost, error) {
315337
if err != nil {
316338
return apiHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err)
317339
}
340+
rawURL, err := url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname()))
341+
if err != nil {
342+
return apiHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err)
343+
}
318344

319345
return apiHost{
320346
baseRESTURL: restURL,
321347
graphqlURL: gqlURL,
322348
uploadURL: uploadURL,
349+
rawURL: rawURL,
323350
}, nil
324351
}
325352

pkg/github/helper_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,36 @@ func getTextResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent {
132132
return textContent
133133
}
134134

135+
func getErrorResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent {
136+
res := getTextResult(t, result)
137+
require.True(t, result.IsError, "expected tool call result to be an error")
138+
return res
139+
}
140+
141+
// getTextResourceResultis a helper function that returns a text result from a tool call.
142+
func getTextResourceResult(t *testing.T, result *mcp.CallToolResult) mcp.TextResourceContents {
143+
t.Helper()
144+
assert.NotNil(t, result)
145+
require.Len(t, result.Content, 2)
146+
content := result.Content[1]
147+
require.IsType(t, mcp.EmbeddedResource{}, content)
148+
resource := content.(mcp.EmbeddedResource)
149+
require.IsType(t, mcp.TextResourceContents{}, resource.Resource)
150+
return resource.Resource.(mcp.TextResourceContents)
151+
}
152+
153+
// getBlobResourceResult is a helper function that returns a blob result from a tool call.
154+
func getBlobResourceResult(t *testing.T, result *mcp.CallToolResult) mcp.BlobResourceContents {
155+
t.Helper()
156+
assert.NotNil(t, result)
157+
require.Len(t, result.Content, 2)
158+
content := result.Content[1]
159+
require.IsType(t, mcp.EmbeddedResource{}, content)
160+
resource := content.(mcp.EmbeddedResource)
161+
require.IsType(t, mcp.BlobResourceContents{}, resource.Resource)
162+
return resource.Resource.(mcp.BlobResourceContents)
163+
}
164+
135165
func TestOptionalParamOK(t *testing.T) {
136166
tests := []struct {
137167
name string

pkg/github/repositories.go

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package github
22

33
import (
44
"context"
5+
"encoding/base64"
56
"encoding/json"
67
"fmt"
78
"io"
89
"net/http"
10+
"net/url"
11+
"strings"
912

13+
"github.com/github/github-mcp-server/pkg/raw"
1014
"github.com/github/github-mcp-server/pkg/translations"
1115
"github.com/google/go-github/v72/github"
1216
"github.com/mark3labs/mcp-go/mcp"
@@ -409,7 +413,7 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun
409413
}
410414

411415
// GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository.
412-
func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
416+
func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
413417
return mcp.NewTool("get_file_contents",
414418
mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")),
415419
mcp.WithToolAnnotation(mcp.ToolAnnotation{
@@ -426,7 +430,7 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc
426430
),
427431
mcp.WithString("path",
428432
mcp.Required(),
429-
mcp.Description("Path to file/directory"),
433+
mcp.Description("Path to file/directory (directories must end with a slash '/')"),
430434
),
431435
mcp.WithString("branch",
432436
mcp.Description("Branch to get contents from"),
@@ -450,38 +454,92 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc
450454
return mcp.NewToolResultError(err.Error()), nil
451455
}
452456

453-
client, err := getClient(ctx)
454-
if err != nil {
455-
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
457+
// 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.
458+
if path != "" && !strings.HasSuffix(path, "/") {
459+
rawOpts := &raw.RawContentOpts{}
460+
if branch != "" {
461+
rawOpts.Ref = "refs/heads/" + branch
462+
}
463+
rawClient, err := getRawClient(ctx)
464+
if err != nil {
465+
return mcp.NewToolResultError("failed to get GitHub raw content client"), nil
466+
}
467+
resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
468+
if err != nil {
469+
return mcp.NewToolResultError("failed to get raw repository content"), nil
470+
}
471+
defer func() {
472+
_ = resp.Body.Close()
473+
}()
474+
475+
if resp.StatusCode != http.StatusOK {
476+
// If the raw content is not found, we will fall back to the GitHub API (in case it is a directory)
477+
} else {
478+
// If the raw content is found, return it directly
479+
body, err := io.ReadAll(resp.Body)
480+
if err != nil {
481+
return mcp.NewToolResultError("failed to read response body"), nil
482+
}
483+
contentType := resp.Header.Get("Content-Type")
484+
485+
var resourceURI string
486+
if branch == "" {
487+
// do a safe url join
488+
resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path)
489+
if err != nil {
490+
return nil, fmt.Errorf("failed to create resource URI: %w", err)
491+
}
492+
} else {
493+
resourceURI, err = url.JoinPath("repo://", owner, repo, "refs", "heads", branch, "contents", path)
494+
if err != nil {
495+
return nil, fmt.Errorf("failed to create resource URI: %w", err)
496+
}
497+
}
498+
if strings.HasPrefix(contentType, "application") || strings.HasPrefix(contentType, "text") {
499+
return mcp.NewToolResultResource("successfully downloaded text file", mcp.TextResourceContents{
500+
URI: resourceURI,
501+
Text: string(body),
502+
MIMEType: contentType,
503+
}), nil
504+
}
505+
506+
return mcp.NewToolResultResource("successfully downloaded binary file", mcp.BlobResourceContents{
507+
URI: resourceURI,
508+
Blob: base64.StdEncoding.EncodeToString(body),
509+
MIMEType: contentType,
510+
}), nil
511+
512+
}
456513
}
457-
opts := &github.RepositoryContentGetOptions{Ref: branch}
458-
fileContent, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
514+
515+
client, err := getClient(ctx)
459516
if err != nil {
460-
return nil, fmt.Errorf("failed to get file contents: %w", err)
517+
return mcp.NewToolResultError("failed to get GitHub client"), nil
461518
}
462-
defer func() { _ = resp.Body.Close() }()
463519

464-
if resp.StatusCode != 200 {
465-
body, err := io.ReadAll(resp.Body)
520+
if strings.HasSuffix(path, "/") {
521+
opts := &github.RepositoryContentGetOptions{Ref: branch}
522+
_, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
466523
if err != nil {
467-
return nil, fmt.Errorf("failed to read response body: %w", err)
524+
return mcp.NewToolResultError("failed to get file contents"), nil
468525
}
469-
return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil
470-
}
526+
defer func() { _ = resp.Body.Close() }()
471527

472-
var result interface{}
473-
if fileContent != nil {
474-
result = fileContent
475-
} else {
476-
result = dirContent
477-
}
528+
if resp.StatusCode != 200 {
529+
body, err := io.ReadAll(resp.Body)
530+
if err != nil {
531+
return mcp.NewToolResultError("failed to read response body"), nil
532+
}
533+
return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil
534+
}
478535

479-
r, err := json.Marshal(result)
480-
if err != nil {
481-
return nil, fmt.Errorf("failed to marshal response: %w", err)
536+
r, err := json.Marshal(dirContent)
537+
if err != nil {
538+
return mcp.NewToolResultError("failed to marshal response"), nil
539+
}
540+
return mcp.NewToolResultText(string(r)), nil
482541
}
483-
484-
return mcp.NewToolResultText(string(r)), nil
542+
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
485543
}
486544
}
487545

0 commit comments

Comments
 (0)