|
8 | 8 | "io"
|
9 | 9 | "net/http"
|
10 | 10 | "net/url"
|
11 |
| -"strconv" |
12 | 11 | "strings"
|
13 | 12 |
|
14 | 13 | ghErrors"github.com/github/github-mcp-server/pkg/errors"
|
@@ -495,33 +494,18 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
|
495 | 494 | returnmcp.NewToolResultError(err.Error()),nil
|
496 | 495 | }
|
497 | 496 |
|
498 |
| -rawOpts:=&raw.ContentOpts{} |
499 |
| - |
500 |
| -ifstrings.HasPrefix(ref,"refs/pull/") { |
501 |
| -prNumber:=strings.TrimSuffix(strings.TrimPrefix(ref,"refs/pull/"),"/head") |
502 |
| -iflen(prNumber)>0 { |
503 |
| -// fetch the PR from the API to get the latest commit and use SHA |
504 |
| -githubClient,err:=getClient(ctx) |
505 |
| -iferr!=nil { |
506 |
| -returnnil,fmt.Errorf("failed to get GitHub client: %w",err) |
507 |
| -} |
508 |
| -prNum,err:=strconv.Atoi(prNumber) |
509 |
| -iferr!=nil { |
510 |
| -returnnil,fmt.Errorf("invalid pull request number: %w",err) |
511 |
| -} |
512 |
| -pr,_,err:=githubClient.PullRequests.Get(ctx,owner,repo,prNum) |
513 |
| -iferr!=nil { |
514 |
| -returnnil,fmt.Errorf("failed to get pull request: %w",err) |
515 |
| -} |
516 |
| -sha=pr.GetHead().GetSHA() |
517 |
| -ref="" |
518 |
| -} |
| 497 | +client,err:=getClient(ctx) |
| 498 | +iferr!=nil { |
| 499 | +returnmcp.NewToolResultError("failed to get GitHub client"),nil |
519 | 500 | }
|
520 | 501 |
|
521 |
| -rawOpts.SHA=sha |
522 |
| -rawOpts.Ref=ref |
| 502 | +rawOpts,err:=resolveGitReference(ctx,client,owner,repo,ref,sha) |
| 503 | +iferr!=nil { |
| 504 | +returnmcp.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s",err)),nil |
| 505 | +} |
523 | 506 |
|
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. |
525 | 509 | ifpath!=""&&!strings.HasSuffix(path,"/") {
|
526 | 510 |
|
527 | 511 | rawClient,err:=getRawClient(ctx)
|
@@ -580,36 +564,51 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
|
580 | 564 | }
|
581 | 565 | }
|
582 | 566 |
|
583 |
| -client,err:=getClient(ctx) |
584 |
| -iferr!=nil { |
585 |
| -returnmcp.NewToolResultError("failed to get GitHub client"),nil |
586 |
| -} |
587 |
| - |
588 |
| -ifsha!="" { |
589 |
| -ref=sha |
| 567 | +ifrawOpts.SHA!="" { |
| 568 | +ref=rawOpts.SHA |
590 | 569 | }
|
591 | 570 | ifstrings.HasSuffix(path,"/") {
|
592 | 571 | opts:=&github.RepositoryContentGetOptions{Ref:ref}
|
593 | 572 | _,dirContent,resp,err:=client.Repositories.GetContents(ctx,owner,repo,path,opts)
|
594 |
| -iferr!=nil { |
595 |
| -returnmcp.NewToolResultError("failed to get file contents"),nil |
596 |
| -} |
597 |
| -deferfunc() {_=resp.Body.Close() }() |
598 |
| - |
599 |
| -ifresp.StatusCode!=200 { |
600 |
| -body,err:=io.ReadAll(resp.Body) |
| 573 | +iferr==nil&&resp.StatusCode==http.StatusOK { |
| 574 | +deferfunc() {_=resp.Body.Close() }() |
| 575 | +r,err:=json.Marshal(dirContent) |
601 | 576 | iferr!=nil {
|
602 |
| -returnmcp.NewToolResultError("failed toread response body"),nil |
| 577 | +returnmcp.NewToolResultError("failed tomarshal response"),nil |
603 | 578 | }
|
604 |
| -returnmcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s",string(body))),nil |
| 579 | +returnmcp.NewToolResultText(string(r)),nil |
605 | 580 | }
|
| 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 | +iferr!=nil { |
| 589 | +returnghErrors.NewGitHubAPIErrorResponse(ctx, |
| 590 | +"failed to get git tree", |
| 591 | +resp, |
| 592 | +err, |
| 593 | +),nil |
| 594 | +} |
| 595 | +deferfunc() {_=resp.Body.Close() }() |
606 | 596 |
|
607 |
| -r,err:=json.Marshal(dirContent) |
| 597 | +// Step 2: Filter tree for matching paths |
| 598 | +constmaxMatchingFiles=3 |
| 599 | +matchingFiles:=filterPaths(tree.Entries,path,maxMatchingFiles) |
| 600 | +iflen(matchingFiles)>0 { |
| 601 | +matchingFilesJSON,err:=json.Marshal(matchingFiles) |
| 602 | +iferr!=nil { |
| 603 | +returnmcp.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s",err)),nil |
| 604 | +} |
| 605 | +resolvedRefs,err:=json.Marshal(rawOpts) |
608 | 606 | iferr!=nil {
|
609 |
| -returnmcp.NewToolResultError("failed to marshalresponse"),nil |
| 607 | +returnmcp.NewToolResultError(fmt.Sprintf("failed to marshalresolved refs: %s",err)),nil |
610 | 608 | }
|
611 |
| -returnmcp.NewToolResultText(string(r)),nil |
| 609 | +returnmcp.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 |
612 | 610 | }
|
| 611 | + |
613 | 612 | returnmcp.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
|
614 | 613 | }
|
615 | 614 | }
|
@@ -1293,3 +1292,74 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool m
|
1293 | 1292 | returnmcp.NewToolResultText(string(r)),nil
|
1294 | 1293 | }
|
1295 | 1294 | }
|
| 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 | +funcfilterPaths(entries []*github.TreeEntry,pathstring,maxResultsint) []string { |
| 1303 | +// Remove trailing slash for matching purposes, but flag whether we |
| 1304 | +// only want directories. |
| 1305 | +dirOnly:=false |
| 1306 | +ifstrings.HasSuffix(path,"/") { |
| 1307 | +dirOnly=true |
| 1308 | +path=strings.TrimSuffix(path,"/") |
| 1309 | +} |
| 1310 | + |
| 1311 | +matchedPaths:= []string{} |
| 1312 | +for_,entry:=rangeentries { |
| 1313 | +iflen(matchedPaths)==maxResults { |
| 1314 | +break// Limit the number of results to maxResults |
| 1315 | +} |
| 1316 | +ifdirOnly&&entry.GetType()!="tree" { |
| 1317 | +continue// Skip non-directory entries if dirOnly is true |
| 1318 | +} |
| 1319 | +entryPath:=entry.GetPath() |
| 1320 | +ifentryPath=="" { |
| 1321 | +continue// Skip empty paths |
| 1322 | +} |
| 1323 | +ifstrings.HasSuffix(entryPath,path) { |
| 1324 | +ifentry.GetType()=="tree" { |
| 1325 | +entryPath+="/"// Return directories with a trailing slash |
| 1326 | +} |
| 1327 | +matchedPaths=append(matchedPaths,entryPath) |
| 1328 | +} |
| 1329 | +} |
| 1330 | +returnmatchedPaths |
| 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 | +funcresolveGitReference(ctx context.Context,githubClient*github.Client,owner,repo,ref,shastring) (*raw.ContentOpts,error) { |
| 1340 | +// 1. If SHA is provided, use it directly |
| 1341 | +ifsha!="" { |
| 1342 | +return&raw.ContentOpts{Ref:"",SHA:sha},nil |
| 1343 | +} |
| 1344 | + |
| 1345 | +// 2. If neither provided, use the default branch as ref |
| 1346 | +ifref=="" { |
| 1347 | +repoInfo,resp,err:=githubClient.Repositories.Get(ctx,owner,repo) |
| 1348 | +iferr!=nil { |
| 1349 | +_,_=ghErrors.NewGitHubAPIErrorToCtx(ctx,"failed to get repository info",resp,err) |
| 1350 | +returnnil,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 | +iferr!=nil { |
| 1358 | +_,_=ghErrors.NewGitHubAPIErrorToCtx(ctx,"failed to get reference",resp,err) |
| 1359 | +returnnil,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 | +} |