Skip to content

Commit fcdbf3e

Browse files
fix: use better raw file handling and return resources
1 parent c423a52 commit fcdbf3e

File tree

18 files changed

+800
-286
lines changed

18 files changed

+800
-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+
// getTextResourceResult is 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)
pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy