chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v89/github"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// CompleteHandler defines function signature for completion handlers
|
||||
type CompleteHandler func(ctx context.Context, client *github.Client, resolved map[string]string, argValue string) ([]string, error)
|
||||
|
||||
// RepositoryResourceArgumentResolvers is a map of argument names to their completion handlers
|
||||
var RepositoryResourceArgumentResolvers = map[string]CompleteHandler{
|
||||
"owner": completeOwner,
|
||||
"repo": completeRepo,
|
||||
"branch": completeBranch,
|
||||
"sha": completeSHA,
|
||||
"tag": completeTag,
|
||||
"prNumber": completePRNumber,
|
||||
"path": completePath,
|
||||
}
|
||||
|
||||
// RepositoryResourceCompletionHandler returns a CompletionHandlerFunc for repository resource completions.
|
||||
func RepositoryResourceCompletionHandler(getClient GetClientFn) func(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
|
||||
return func(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
|
||||
if req.Params.Ref.Type != "ref/resource" {
|
||||
return nil, nil // Not a resource completion
|
||||
}
|
||||
|
||||
argName := req.Params.Argument.Name
|
||||
argValue := req.Params.Argument.Value
|
||||
var resolved map[string]string
|
||||
if req.Params.Context != nil && req.Params.Context.Arguments != nil {
|
||||
resolved = req.Params.Context.Arguments
|
||||
} else {
|
||||
resolved = map[string]string{}
|
||||
}
|
||||
|
||||
client, err := getClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Argument resolver functions
|
||||
resolvers := RepositoryResourceArgumentResolvers
|
||||
|
||||
resolver, ok := resolvers[argName]
|
||||
if !ok {
|
||||
return nil, errors.New("no resolver for argument: " + argName)
|
||||
}
|
||||
|
||||
values, err := resolver(ctx, client, resolved, argValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(values) > 100 {
|
||||
values = values[:100]
|
||||
}
|
||||
|
||||
return &mcp.CompleteResult{
|
||||
Completion: mcp.CompletionResultDetails{
|
||||
Values: values,
|
||||
Total: len(values),
|
||||
HasMore: false,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// --- Per-argument resolver functions ---
|
||||
|
||||
func completeOwner(ctx context.Context, client *github.Client, _ map[string]string, argValue string) ([]string, error) {
|
||||
var values []string
|
||||
user, _, err := client.Users.Get(ctx, "")
|
||||
if err == nil && user.GetLogin() != "" {
|
||||
values = append(values, user.GetLogin())
|
||||
}
|
||||
|
||||
orgs, _, err := client.Organizations.List(ctx, "", &github.ListOptions{PerPage: 100})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, org := range orgs {
|
||||
values = append(values, org.GetLogin())
|
||||
}
|
||||
|
||||
// filter values based on argValue and replace values slice
|
||||
if argValue != "" {
|
||||
var filteredValues []string
|
||||
for _, value := range values {
|
||||
if strings.Contains(value, argValue) {
|
||||
filteredValues = append(filteredValues, value)
|
||||
}
|
||||
}
|
||||
values = filteredValues
|
||||
}
|
||||
if len(values) > 100 {
|
||||
values = values[:100]
|
||||
return values, nil // Limit to 100 results
|
||||
}
|
||||
// Else also do a client.Search.Users()
|
||||
if argValue == "" {
|
||||
return values, nil // No need to search if no argValue
|
||||
}
|
||||
users, _, err := client.Search.Users(ctx, argValue, &github.SearchOptions{ListOptions: github.ListOptions{PerPage: 100 - len(values)}})
|
||||
if err != nil || users == nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users.Users {
|
||||
values = append(values, user.GetLogin())
|
||||
}
|
||||
|
||||
if len(values) > 100 {
|
||||
values = values[:100]
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func completeRepo(ctx context.Context, client *github.Client, resolved map[string]string, argValue string) ([]string, error) {
|
||||
var values []string
|
||||
owner := resolved["owner"]
|
||||
if owner == "" {
|
||||
return values, errors.New("owner not specified")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("org:%s", owner)
|
||||
|
||||
if argValue != "" {
|
||||
query = fmt.Sprintf("%s %s", query, argValue)
|
||||
}
|
||||
repos, _, err := client.Search.Repositories(ctx, query, &github.SearchOptions{ListOptions: github.ListOptions{PerPage: 100}})
|
||||
if err != nil || repos == nil {
|
||||
return values, errors.New("failed to get repositories")
|
||||
}
|
||||
// filter repos based on argValue
|
||||
for _, repo := range repos.Repositories {
|
||||
name := repo.GetName()
|
||||
if argValue == "" || strings.HasPrefix(name, argValue) {
|
||||
values = append(values, name)
|
||||
}
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func completeBranch(ctx context.Context, client *github.Client, resolved map[string]string, argValue string) ([]string, error) {
|
||||
var values []string
|
||||
owner := resolved["owner"]
|
||||
repo := resolved["repo"]
|
||||
if owner == "" || repo == "" {
|
||||
return values, errors.New("owner or repo not specified")
|
||||
}
|
||||
branches, _, _ := client.Repositories.ListBranches(ctx, owner, repo, nil)
|
||||
|
||||
for _, branch := range branches {
|
||||
if argValue == "" || strings.HasPrefix(branch.GetName(), argValue) {
|
||||
values = append(values, branch.GetName())
|
||||
}
|
||||
}
|
||||
if len(values) > 100 {
|
||||
values = values[:100]
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func completeSHA(ctx context.Context, client *github.Client, resolved map[string]string, argValue string) ([]string, error) {
|
||||
var values []string
|
||||
owner := resolved["owner"]
|
||||
repo := resolved["repo"]
|
||||
if owner == "" || repo == "" {
|
||||
return values, errors.New("owner or repo not specified")
|
||||
}
|
||||
commits, _, _ := client.Repositories.ListCommits(ctx, owner, repo, nil)
|
||||
|
||||
for _, commit := range commits {
|
||||
sha := commit.GetSHA()
|
||||
if argValue == "" || strings.HasPrefix(sha, argValue) {
|
||||
values = append(values, sha)
|
||||
}
|
||||
}
|
||||
if len(values) > 100 {
|
||||
values = values[:100]
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func completeTag(ctx context.Context, client *github.Client, resolved map[string]string, argValue string) ([]string, error) {
|
||||
owner := resolved["owner"]
|
||||
repo := resolved["repo"]
|
||||
if owner == "" || repo == "" {
|
||||
return nil, errors.New("owner or repo not specified")
|
||||
}
|
||||
tags, _, _ := client.Repositories.ListTags(ctx, owner, repo, nil)
|
||||
var values []string
|
||||
for _, tag := range tags {
|
||||
if argValue == "" || strings.Contains(tag.GetName(), argValue) {
|
||||
values = append(values, tag.GetName())
|
||||
}
|
||||
}
|
||||
if len(values) > 100 {
|
||||
values = values[:100]
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func completePRNumber(ctx context.Context, client *github.Client, resolved map[string]string, argValue string) ([]string, error) {
|
||||
var values []string
|
||||
owner := resolved["owner"]
|
||||
repo := resolved["repo"]
|
||||
if owner == "" || repo == "" {
|
||||
return values, errors.New("owner or repo not specified")
|
||||
}
|
||||
|
||||
prs, _, err := client.Search.Issues(ctx, fmt.Sprintf("repo:%s/%s is:open is:pr", owner, repo), &github.SearchOptions{ListOptions: github.ListOptions{PerPage: 100}})
|
||||
if err != nil {
|
||||
return values, err
|
||||
}
|
||||
for _, pr := range prs.Issues {
|
||||
num := fmt.Sprintf("%d", pr.GetNumber())
|
||||
if argValue == "" || strings.HasPrefix(num, argValue) {
|
||||
values = append(values, num)
|
||||
}
|
||||
}
|
||||
if len(values) > 100 {
|
||||
values = values[:100]
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func completePath(ctx context.Context, client *github.Client, resolved map[string]string, argValue string) ([]string, error) {
|
||||
owner := resolved["owner"]
|
||||
repo := resolved["repo"]
|
||||
if owner == "" || repo == "" {
|
||||
return nil, errors.New("owner or repo not specified")
|
||||
}
|
||||
refVal := resolved["branch"]
|
||||
if refVal == "" {
|
||||
refVal = resolved["sha"]
|
||||
}
|
||||
if refVal == "" {
|
||||
refVal = resolved["tag"]
|
||||
}
|
||||
if refVal == "" {
|
||||
refVal = "HEAD"
|
||||
}
|
||||
|
||||
// Determine the prefix to complete (directory path or file path)
|
||||
prefix := argValue
|
||||
if prefix != "" && !strings.HasSuffix(prefix, "/") {
|
||||
lastSlash := strings.LastIndex(prefix, "/")
|
||||
if lastSlash >= 0 {
|
||||
prefix = prefix[:lastSlash+1]
|
||||
} else {
|
||||
prefix = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Get the tree for the ref (recursive)
|
||||
tree, _, err := client.Git.GetTree(ctx, owner, repo, refVal, true)
|
||||
if err != nil || tree == nil {
|
||||
return nil, errors.New("failed to get file tree")
|
||||
}
|
||||
|
||||
// Collect immediate children of the prefix (files and directories, no duplicates)
|
||||
dirs := map[string]struct{}{}
|
||||
files := map[string]struct{}{}
|
||||
prefixLen := len(prefix)
|
||||
for _, entry := range tree.Entries {
|
||||
if !strings.HasPrefix(entry.GetPath(), prefix) {
|
||||
continue
|
||||
}
|
||||
rel := entry.GetPath()[prefixLen:]
|
||||
if rel == "" {
|
||||
continue
|
||||
}
|
||||
// Only immediate children
|
||||
slashIdx := strings.Index(rel, "/")
|
||||
if slashIdx >= 0 {
|
||||
// Directory: only add the directory name (with trailing slash), prefixed with full path
|
||||
dirName := prefix + rel[:slashIdx+1]
|
||||
dirs[dirName] = struct{}{}
|
||||
} else if entry.GetType() == "blob" {
|
||||
// File: add as-is, prefixed with full path
|
||||
fileName := prefix + rel
|
||||
files[fileName] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally filter by argValue (if user is typing after last slash)
|
||||
var filter string
|
||||
if argValue != "" {
|
||||
if lastSlash := strings.LastIndex(argValue, "/"); lastSlash >= 0 {
|
||||
filter = argValue[lastSlash+1:]
|
||||
} else {
|
||||
filter = argValue
|
||||
}
|
||||
}
|
||||
|
||||
var values []string
|
||||
// Add directories first, then files, both filtered
|
||||
for dir := range dirs {
|
||||
// Only filter on the last segment after the last slash
|
||||
if filter == "" {
|
||||
values = append(values, dir)
|
||||
} else {
|
||||
last := dir
|
||||
if idx := strings.LastIndex(strings.TrimRight(dir, "/"), "/"); idx >= 0 {
|
||||
last = dir[idx+1:]
|
||||
}
|
||||
if strings.HasPrefix(last, filter) {
|
||||
values = append(values, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
for file := range files {
|
||||
if filter == "" {
|
||||
values = append(values, file)
|
||||
} else {
|
||||
last := file
|
||||
if idx := strings.LastIndex(file, "/"); idx >= 0 {
|
||||
last = file[idx+1:]
|
||||
}
|
||||
if strings.HasPrefix(last, filter) {
|
||||
values = append(values, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) > 100 {
|
||||
values = values[:100]
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
Reference in New Issue
Block a user