338 lines
9.2 KiB
Go
338 lines
9.2 KiB
Go
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
|
|
}
|