Skip to content

Commit 5f100d8

Browse files
authored
Merge branch 'main' into fix_ghes_hostname
2 parents 4d53e81 + d15026b commit 5f100d8

File tree

6 files changed

+324
-53
lines changed

6 files changed

+324
-53
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
899899

900900
- **get_file_contents** - Get file or directory contents
901901
- `owner`: Repository owner (username or organization) (string, required)
902-
- `path`: Path to file/directory (directories must end with a slash '/') (string, required)
902+
- `path`: Path to file/directory (directories must end with a slash '/') (string, optional)
903903
- `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional)
904904
- `repo`: Repository name (string, required)
905905
- `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional)

pkg/github/__toolsnaps__/get_file_contents.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"type": "string"
1212
},
1313
"path": {
14+
"default": "/",
1415
"description": "Path to file/directory (directories must end with a slash '/')",
1516
"type": "string"
1617
},
@@ -29,8 +30,7 @@
2930
},
3031
"required": [
3132
"owner",
32-
"repo",
33-
"path"
33+
"repo"
3434
],
3535
"type": "object"
3636
},

pkg/github/__toolsnaps__/list_pull_requests.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"title": "List pull requests",
44
"readOnlyHint": true
55
},
6-
"description": "List pull requests in a GitHub repository.",
6+
"description": "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.",
77
"inputSchema": {
88
"properties": {
99
"base": {

pkg/github/pullrequests.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu
330330
// ListPullRequests creates a tool to list and filter repository pull requests.
331331
func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
332332
return mcp.NewTool("list_pull_requests",
333-
mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository.")),
333+
mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.")),
334334
mcp.WithToolAnnotation(mcp.ToolAnnotation{
335335
Title: t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"),
336336
ReadOnlyHint: ToBoolPtr(true),
@@ -396,7 +396,6 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun
396396
if err != nil {
397397
return mcp.NewToolResultError(err.Error()), nil
398398
}
399-
400399
opts := &github.PullRequestListOptions{
401400
State: state,
402401
Head: head,

pkg/github/repositories.go

Lines changed: 115 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"io"
99
"net/http"
1010
"net/url"
11-
"strconv"
1211
"strings"
1312

1413
ghErrors "github.com/github/github-mcp-server/pkg/errors"
@@ -463,8 +462,8 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
463462
mcp.Description("Repository name"),
464463
),
465464
mcp.WithString("path",
466-
mcp.Required(),
467465
mcp.Description("Path to file/directory (directories must end with a slash '/')"),
466+
mcp.DefaultString("/"),
468467
),
469468
mcp.WithString("ref",
470469
mcp.Description("Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`"),
@@ -495,33 +494,18 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
495494
return mcp.NewToolResultError(err.Error()), nil
496495
}
497496

498-
rawOpts := &raw.ContentOpts{}
499-
500-
if strings.HasPrefix(ref, "refs/pull/") {
501-
prNumber := strings.TrimSuffix(strings.TrimPrefix(ref, "refs/pull/"), "/head")
502-
if len(prNumber) > 0 {
503-
// fetch the PR from the API to get the latest commit and use SHA
504-
githubClient, err := getClient(ctx)
505-
if err != nil {
506-
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
507-
}
508-
prNum, err := strconv.Atoi(prNumber)
509-
if err != nil {
510-
return nil, fmt.Errorf("invalid pull request number: %w", err)
511-
}
512-
pr, _, err := githubClient.PullRequests.Get(ctx, owner, repo, prNum)
513-
if err != nil {
514-
return nil, fmt.Errorf("failed to get pull request: %w", err)
515-
}
516-
sha = pr.GetHead().GetSHA()
517-
ref = ""
518-
}
497+
client, err := getClient(ctx)
498+
if err != nil {
499+
return mcp.NewToolResultError("failed to get GitHub client"), nil
519500
}
520501

521-
rawOpts.SHA = sha
522-
rawOpts.Ref = ref
502+
rawOpts, err := resolveGitReference(ctx, client, owner, repo, ref, sha)
503+
if err != nil {
504+
return mcp.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil
505+
}
523506

524-
// 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.
507+
// If the path is (most likely) not to be a directory, we will
508+
// first try to get the raw content from the GitHub raw content API.
525509
if path != "" && !strings.HasSuffix(path, "/") {
526510

527511
rawClient, err := getRawClient(ctx)
@@ -580,36 +564,51 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
580564
}
581565
}
582566

583-
client, err := getClient(ctx)
584-
if err != nil {
585-
return mcp.NewToolResultError("failed to get GitHub client"), nil
586-
}
587-
588-
if sha != "" {
589-
ref = sha
567+
if rawOpts.SHA != "" {
568+
ref = rawOpts.SHA
590569
}
591570
if strings.HasSuffix(path, "/") {
592571
opts := &github.RepositoryContentGetOptions{Ref: ref}
593572
_, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
594-
if err != nil {
595-
return mcp.NewToolResultError("failed to get file contents"), nil
596-
}
597-
defer func() { _ = resp.Body.Close() }()
598-
599-
if resp.StatusCode != 200 {
600-
body, err := io.ReadAll(resp.Body)
573+
if err == nil && resp.StatusCode == http.StatusOK {
574+
defer func() { _ = resp.Body.Close() }()
575+
r, err := json.Marshal(dirContent)
601576
if err != nil {
602-
return mcp.NewToolResultError("failed to read response body"), nil
577+
return mcp.NewToolResultError("failed to marshal response"), nil
603578
}
604-
return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil
579+
return mcp.NewToolResultText(string(r)), nil
605580
}
581+
}
582+
583+
// The path does not point to a file or directory.
584+
// Instead let's try to find it in the Git Tree by matching the end of the path.
585+
586+
// Step 1: Get Git Tree recursively
587+
tree, resp, err := client.Git.GetTree(ctx, owner, repo, ref, true)
588+
if err != nil {
589+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
590+
"failed to get git tree",
591+
resp,
592+
err,
593+
), nil
594+
}
595+
defer func() { _ = resp.Body.Close() }()
606596

607-
r, err := json.Marshal(dirContent)
597+
// Step 2: Filter tree for matching paths
598+
const maxMatchingFiles = 3
599+
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
600+
if len(matchingFiles) > 0 {
601+
matchingFilesJSON, err := json.Marshal(matchingFiles)
602+
if err != nil {
603+
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil
604+
}
605+
resolvedRefs, err := json.Marshal(rawOpts)
608606
if err != nil {
609-
return mcp.NewToolResultError("failed to marshal response"), nil
607+
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil
610608
}
611-
return mcp.NewToolResultText(string(r)), nil
609+
return mcp.NewToolResultText(fmt.Sprintf("Path did not point to a file or directory, but resolved git ref to %s with possible path matches: %s", resolvedRefs, matchingFilesJSON)), nil
612610
}
611+
613612
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
614613
}
615614
}
@@ -1293,3 +1292,74 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool m
12931292
return mcp.NewToolResultText(string(r)), nil
12941293
}
12951294
}
1295+
1296+
// filterPaths filters the entries in a GitHub tree to find paths that
1297+
// match the given suffix.
1298+
// maxResults limits the number of results returned to first maxResults entries,
1299+
// a maxResults of -1 means no limit.
1300+
// It returns a slice of strings containing the matching paths.
1301+
// Directories are returned with a trailing slash.
1302+
func filterPaths(entries []*github.TreeEntry, path string, maxResults int) []string {
1303+
// Remove trailing slash for matching purposes, but flag whether we
1304+
// only want directories.
1305+
dirOnly := false
1306+
if strings.HasSuffix(path, "/") {
1307+
dirOnly = true
1308+
path = strings.TrimSuffix(path, "/")
1309+
}
1310+
1311+
matchedPaths := []string{}
1312+
for _, entry := range entries {
1313+
if len(matchedPaths) == maxResults {
1314+
break // Limit the number of results to maxResults
1315+
}
1316+
if dirOnly && entry.GetType() != "tree" {
1317+
continue // Skip non-directory entries if dirOnly is true
1318+
}
1319+
entryPath := entry.GetPath()
1320+
if entryPath == "" {
1321+
continue // Skip empty paths
1322+
}
1323+
if strings.HasSuffix(entryPath, path) {
1324+
if entry.GetType() == "tree" {
1325+
entryPath += "/" // Return directories with a trailing slash
1326+
}
1327+
matchedPaths = append(matchedPaths, entryPath)
1328+
}
1329+
}
1330+
return matchedPaths
1331+
}
1332+
1333+
// resolveGitReference resolves git references with the following logic:
1334+
// 1. If SHA is provided, it takes precedence
1335+
// 2. If neither is provided, use the default branch as ref
1336+
// 3. Get commit SHA from the ref
1337+
// Refs can look like `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`
1338+
// The function returns the resolved ref, commit SHA and any error.
1339+
func resolveGitReference(ctx context.Context, githubClient *github.Client, owner, repo, ref, sha string) (*raw.ContentOpts, error) {
1340+
// 1. If SHA is provided, use it directly
1341+
if sha != "" {
1342+
return &raw.ContentOpts{Ref: "", SHA: sha}, nil
1343+
}
1344+
1345+
// 2. If neither provided, use the default branch as ref
1346+
if ref == "" {
1347+
repoInfo, resp, err := githubClient.Repositories.Get(ctx, owner, repo)
1348+
if err != nil {
1349+
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get repository info", resp, err)
1350+
return nil, fmt.Errorf("failed to get repository info: %w", err)
1351+
}
1352+
ref = fmt.Sprintf("refs/heads/%s", repoInfo.GetDefaultBranch())
1353+
}
1354+
1355+
// 3. Get the SHA from the ref
1356+
reference, resp, err := githubClient.Git.GetRef(ctx, owner, repo, ref)
1357+
if err != nil {
1358+
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get reference", resp, err)
1359+
return nil, fmt.Errorf("failed to get reference: %w", err)
1360+
}
1361+
sha = reference.GetObject().GetSHA()
1362+
1363+
// Use provided ref, or it will be empty which defaults to the default branch
1364+
return &raw.ContentOpts{Ref: ref, SHA: sha}, nil
1365+
}

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