Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitfa2d802

Browse files
Add resource completion for GitHub repository resources (#1493)
Port resource completion from the remote GitHub MCP Server---Co-authored-by: Ksenia Bobrova <almaleksia@github.com>
1 parent9b34211 commitfa2d802

File tree

4 files changed

+735
-12
lines changed

4 files changed

+735
-12
lines changed

‎internal/ghmcp/server.go‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,6 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
124124
// Generate instructions based on enabled toolsets
125125
instructions:=github.GenerateInstructions(enabledToolsets)
126126

127-
ghServer:=github.NewServer(cfg.Version,&mcp.ServerOptions{
128-
Instructions:instructions,
129-
HasTools:true,
130-
HasResources:true,
131-
HasPrompts:true,
132-
Logger:cfg.Logger,
133-
})
134-
135-
// Add middlewares
136-
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
137-
ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg,restClient,gqlHTTPClient))
138-
139127
getClient:=func(_ context.Context) (*gogithub.Client,error) {
140128
returnrestClient,nil// closing over client
141129
}
@@ -152,6 +140,16 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
152140
returnraw.NewClient(client,apiHost.rawURL),nil// closing over client
153141
}
154142

143+
ghServer:=github.NewServer(cfg.Version,&mcp.ServerOptions{
144+
Instructions:instructions,
145+
Logger:cfg.Logger,
146+
CompletionHandler:github.CompletionsHandler(getClient),
147+
})
148+
149+
// Add middlewares
150+
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
151+
ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg,restClient,gqlHTTPClient))
152+
155153
// Create default toolsets
156154
tsg:=github.DefaultToolsetGroup(
157155
cfg.ReadOnly,
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/google/go-github/v79/github"
10+
"github.com/modelcontextprotocol/go-sdk/mcp"
11+
)
12+
13+
// CompleteHandler defines function signature for completion handlers
14+
typeCompleteHandlerfunc(ctx context.Context,client*github.Client,resolvedmap[string]string,argValuestring) ([]string,error)
15+
16+
// RepositoryResourceArgumentResolvers is a map of argument names to their completion handlers
17+
varRepositoryResourceArgumentResolvers=map[string]CompleteHandler{
18+
"owner":completeOwner,
19+
"repo":completeRepo,
20+
"branch":completeBranch,
21+
"sha":completeSHA,
22+
"tag":completeTag,
23+
"prNumber":completePRNumber,
24+
"path":completePath,
25+
}
26+
27+
// RepositoryResourceCompletionHandler returns a CompletionHandlerFunc for repository resource completions.
28+
funcRepositoryResourceCompletionHandler(getClientGetClientFn)func(ctx context.Context,req*mcp.CompleteRequest) (*mcp.CompleteResult,error) {
29+
returnfunc(ctx context.Context,req*mcp.CompleteRequest) (*mcp.CompleteResult,error) {
30+
ifreq.Params.Ref.Type!="ref/resource" {
31+
returnnil,nil// Not a resource completion
32+
}
33+
34+
argName:=req.Params.Argument.Name
35+
argValue:=req.Params.Argument.Value
36+
resolved:=req.Params.Context.Arguments
37+
ifresolved==nil {
38+
resolved=map[string]string{}
39+
}
40+
41+
client,err:=getClient(ctx)
42+
iferr!=nil {
43+
returnnil,err
44+
}
45+
46+
// Argument resolver functions
47+
resolvers:=RepositoryResourceArgumentResolvers
48+
49+
resolver,ok:=resolvers[argName]
50+
if!ok {
51+
returnnil,errors.New("no resolver for argument: "+argName)
52+
}
53+
54+
values,err:=resolver(ctx,client,resolved,argValue)
55+
iferr!=nil {
56+
returnnil,err
57+
}
58+
iflen(values)>100 {
59+
values=values[:100]
60+
}
61+
62+
return&mcp.CompleteResult{
63+
Completion: mcp.CompletionResultDetails{
64+
Values:values,
65+
Total:len(values),
66+
HasMore:false,
67+
},
68+
},nil
69+
}
70+
}
71+
72+
// --- Per-argument resolver functions ---
73+
74+
funccompleteOwner(ctx context.Context,client*github.Client,_map[string]string,argValuestring) ([]string,error) {
75+
varvalues []string
76+
user,_,err:=client.Users.Get(ctx,"")
77+
iferr==nil&&user.GetLogin()!="" {
78+
values=append(values,user.GetLogin())
79+
}
80+
81+
orgs,_,err:=client.Organizations.List(ctx,"",&github.ListOptions{PerPage:100})
82+
iferr!=nil {
83+
returnnil,err
84+
}
85+
for_,org:=rangeorgs {
86+
values=append(values,org.GetLogin())
87+
}
88+
89+
// filter values based on argValue and replace values slice
90+
ifargValue!="" {
91+
varfilteredValues []string
92+
for_,value:=rangevalues {
93+
ifstrings.Contains(value,argValue) {
94+
filteredValues=append(filteredValues,value)
95+
}
96+
}
97+
values=filteredValues
98+
}
99+
iflen(values)>100 {
100+
values=values[:100]
101+
returnvalues,nil// Limit to 100 results
102+
}
103+
// Else also do a client.Search.Users()
104+
ifargValue=="" {
105+
returnvalues,nil// No need to search if no argValue
106+
}
107+
users,_,err:=client.Search.Users(ctx,argValue,&github.SearchOptions{ListOptions: github.ListOptions{PerPage:100-len(values)}})
108+
iferr!=nil||users==nil {
109+
returnnil,err
110+
}
111+
for_,user:=rangeusers.Users {
112+
values=append(values,user.GetLogin())
113+
}
114+
115+
iflen(values)>100 {
116+
values=values[:100]
117+
}
118+
returnvalues,nil
119+
}
120+
121+
funccompleteRepo(ctx context.Context,client*github.Client,resolvedmap[string]string,argValuestring) ([]string,error) {
122+
varvalues []string
123+
owner:=resolved["owner"]
124+
ifowner=="" {
125+
returnvalues,errors.New("owner not specified")
126+
}
127+
128+
query:=fmt.Sprintf("org:%s",owner)
129+
130+
ifargValue!="" {
131+
query=fmt.Sprintf("%s %s",query,argValue)
132+
}
133+
repos,_,err:=client.Search.Repositories(ctx,query,&github.SearchOptions{ListOptions: github.ListOptions{PerPage:100}})
134+
iferr!=nil||repos==nil {
135+
returnvalues,errors.New("failed to get repositories")
136+
}
137+
// filter repos based on argValue
138+
for_,repo:=rangerepos.Repositories {
139+
name:=repo.GetName()
140+
ifargValue==""||strings.HasPrefix(name,argValue) {
141+
values=append(values,name)
142+
}
143+
}
144+
145+
returnvalues,nil
146+
}
147+
148+
funccompleteBranch(ctx context.Context,client*github.Client,resolvedmap[string]string,argValuestring) ([]string,error) {
149+
varvalues []string
150+
owner:=resolved["owner"]
151+
repo:=resolved["repo"]
152+
ifowner==""||repo=="" {
153+
returnvalues,errors.New("owner or repo not specified")
154+
}
155+
branches,_,_:=client.Repositories.ListBranches(ctx,owner,repo,nil)
156+
157+
for_,branch:=rangebranches {
158+
ifargValue==""||strings.HasPrefix(branch.GetName(),argValue) {
159+
values=append(values,branch.GetName())
160+
}
161+
}
162+
iflen(values)>100 {
163+
values=values[:100]
164+
}
165+
returnvalues,nil
166+
}
167+
168+
funccompleteSHA(ctx context.Context,client*github.Client,resolvedmap[string]string,argValuestring) ([]string,error) {
169+
varvalues []string
170+
owner:=resolved["owner"]
171+
repo:=resolved["repo"]
172+
ifowner==""||repo=="" {
173+
returnvalues,errors.New("owner or repo not specified")
174+
}
175+
commits,_,_:=client.Repositories.ListCommits(ctx,owner,repo,nil)
176+
177+
for_,commit:=rangecommits {
178+
sha:=commit.GetSHA()
179+
ifargValue==""||strings.HasPrefix(sha,argValue) {
180+
values=append(values,sha)
181+
}
182+
}
183+
iflen(values)>100 {
184+
values=values[:100]
185+
}
186+
returnvalues,nil
187+
}
188+
189+
funccompleteTag(ctx context.Context,client*github.Client,resolvedmap[string]string,argValuestring) ([]string,error) {
190+
owner:=resolved["owner"]
191+
repo:=resolved["repo"]
192+
ifowner==""||repo=="" {
193+
returnnil,errors.New("owner or repo not specified")
194+
}
195+
tags,_,_:=client.Repositories.ListTags(ctx,owner,repo,nil)
196+
varvalues []string
197+
for_,tag:=rangetags {
198+
ifargValue==""||strings.Contains(tag.GetName(),argValue) {
199+
values=append(values,tag.GetName())
200+
}
201+
}
202+
iflen(values)>100 {
203+
values=values[:100]
204+
}
205+
returnvalues,nil
206+
}
207+
208+
funccompletePRNumber(ctx context.Context,client*github.Client,resolvedmap[string]string,argValuestring) ([]string,error) {
209+
varvalues []string
210+
owner:=resolved["owner"]
211+
repo:=resolved["repo"]
212+
ifowner==""||repo=="" {
213+
returnvalues,errors.New("owner or repo not specified")
214+
}
215+
216+
prs,_,err:=client.Search.Issues(ctx,fmt.Sprintf("repo:%s/%s is:open is:pr",owner,repo),&github.SearchOptions{ListOptions: github.ListOptions{PerPage:100}})
217+
iferr!=nil {
218+
returnvalues,err
219+
}
220+
for_,pr:=rangeprs.Issues {
221+
num:=fmt.Sprintf("%d",pr.GetNumber())
222+
ifargValue==""||strings.HasPrefix(num,argValue) {
223+
values=append(values,num)
224+
}
225+
}
226+
iflen(values)>100 {
227+
values=values[:100]
228+
}
229+
returnvalues,nil
230+
}
231+
232+
funccompletePath(ctx context.Context,client*github.Client,resolvedmap[string]string,argValuestring) ([]string,error) {
233+
owner:=resolved["owner"]
234+
repo:=resolved["repo"]
235+
ifowner==""||repo=="" {
236+
returnnil,errors.New("owner or repo not specified")
237+
}
238+
refVal:=resolved["branch"]
239+
ifrefVal=="" {
240+
refVal=resolved["sha"]
241+
}
242+
ifrefVal=="" {
243+
refVal=resolved["tag"]
244+
}
245+
ifrefVal=="" {
246+
refVal="HEAD"
247+
}
248+
249+
// Determine the prefix to complete (directory path or file path)
250+
prefix:=argValue
251+
ifprefix!=""&&!strings.HasSuffix(prefix,"/") {
252+
lastSlash:=strings.LastIndex(prefix,"/")
253+
iflastSlash>=0 {
254+
prefix=prefix[:lastSlash+1]
255+
}else {
256+
prefix=""
257+
}
258+
}
259+
260+
// Get the tree for the ref (recursive)
261+
tree,_,err:=client.Git.GetTree(ctx,owner,repo,refVal,true)
262+
iferr!=nil||tree==nil {
263+
returnnil,errors.New("failed to get file tree")
264+
}
265+
266+
// Collect immediate children of the prefix (files and directories, no duplicates)
267+
dirs:=map[string]struct{}{}
268+
files:=map[string]struct{}{}
269+
prefixLen:=len(prefix)
270+
for_,entry:=rangetree.Entries {
271+
if!strings.HasPrefix(entry.GetPath(),prefix) {
272+
continue
273+
}
274+
rel:=entry.GetPath()[prefixLen:]
275+
ifrel=="" {
276+
continue
277+
}
278+
// Only immediate children
279+
slashIdx:=strings.Index(rel,"/")
280+
ifslashIdx>=0 {
281+
// Directory: only add the directory name (with trailing slash), prefixed with full path
282+
dirName:=prefix+rel[:slashIdx+1]
283+
dirs[dirName]=struct{}{}
284+
}elseifentry.GetType()=="blob" {
285+
// File: add as-is, prefixed with full path
286+
fileName:=prefix+rel
287+
files[fileName]=struct{}{}
288+
}
289+
}
290+
291+
// Optionally filter by argValue (if user is typing after last slash)
292+
varfilterstring
293+
ifargValue!="" {
294+
iflastSlash:=strings.LastIndex(argValue,"/");lastSlash>=0 {
295+
filter=argValue[lastSlash+1:]
296+
}else {
297+
filter=argValue
298+
}
299+
}
300+
301+
varvalues []string
302+
// Add directories first, then files, both filtered
303+
fordir:=rangedirs {
304+
// Only filter on the last segment after the last slash
305+
iffilter=="" {
306+
values=append(values,dir)
307+
}else {
308+
last:=dir
309+
ifidx:=strings.LastIndex(strings.TrimRight(dir,"/"),"/");idx>=0 {
310+
last=dir[idx+1:]
311+
}
312+
ifstrings.HasPrefix(last,filter) {
313+
values=append(values,dir)
314+
}
315+
}
316+
}
317+
forfile:=rangefiles {
318+
iffilter=="" {
319+
values=append(values,file)
320+
}else {
321+
last:=file
322+
ifidx:=strings.LastIndex(file,"/");idx>=0 {
323+
last=file[idx+1:]
324+
}
325+
ifstrings.HasPrefix(last,filter) {
326+
values=append(values,file)
327+
}
328+
}
329+
}
330+
331+
iflen(values)>100 {
332+
values=values[:100]
333+
}
334+
returnvalues,nil
335+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp