Skip to content

Commit d99a34b

Browse files
committed
Add ifc label for search_repositories tool
Emits an IFC SecurityLabel on the search_repositories tool result when the InsidersMode flag is enabled, mirroring the pattern landed for get_me (#2432), list_issues (#2453), get_file_contents (#2454), search_issues (#2456), and issue_read (#2457). Search results may span multiple repositories, so the join math (integrity always untrusted; private wins by intersecting collaborator sets across the matched private repos only) is shared with search_issues via ifc.LabelSearchIssues. Visibility is read directly off the search response's repo.Private field — no extra API call. Collaborators are fetched only for private hits, and any failure causes the label to be omitted entirely (consistent with search_issues / issue_read / get_file_contents). Refs github/copilot-mcp-core#1623, github/copilot-mcp-core#1389.
1 parent 883f58d commit d99a34b

2 files changed

Lines changed: 229 additions & 1 deletion

File tree

pkg/github/search.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99

1010
ghErrors "github.com/github/github-mcp-server/pkg/errors"
11+
"github.com/github/github-mcp-server/pkg/ifc"
1112
"github.com/github/github-mcp-server/pkg/inventory"
1213
"github.com/github/github-mcp-server/pkg/scopes"
1314
"github.com/github/github-mcp-server/pkg/translations"
@@ -161,11 +162,60 @@ func SearchRepositories(t translations.TranslationHelperFunc) inventory.ServerTo
161162
}
162163
}
163164

164-
return utils.NewToolResultText(string(r)), nil, nil
165+
callResult := utils.NewToolResultText(string(r))
166+
if deps.GetFlags(ctx).InsidersMode {
167+
attachSearchRepositoriesIFCLabel(ctx, client, result.Repositories, callResult)
168+
}
169+
return callResult, nil, nil
165170
},
166171
)
167172
}
168173

174+
// attachSearchRepositoriesIFCLabel joins per-repository IFC labels across
175+
// every matched repository and attaches the result to callResult. Visibility
176+
// is read directly from the search response (no extra API call); collaborators
177+
// are fetched once per private repository. If any collaborators lookup fails
178+
// the label is omitted to avoid misclassifying the result. The join math is
179+
// shared with search_issues via ifc.LabelSearchIssues: integrity is always
180+
// untrusted, and confidentiality is the intersection of the reader sets of
181+
// the matched private repositories (public matches contribute the universe
182+
// set and drop out without shrinking it).
183+
func attachSearchRepositoriesIFCLabel(ctx context.Context, client *github.Client, repos []*github.Repository, callResult *mcp.CallToolResult) {
184+
if callResult == nil || callResult.IsError {
185+
return
186+
}
187+
188+
visibilities := make([]bool, 0, len(repos))
189+
readerSets := make([][]string, 0, len(repos))
190+
for _, repo := range repos {
191+
isPrivate := repo.GetPrivate()
192+
visibilities = append(visibilities, isPrivate)
193+
if !isPrivate {
194+
readerSets = append(readerSets, nil)
195+
continue
196+
}
197+
owner := repo.GetOwner().GetLogin()
198+
name := repo.GetName()
199+
if owner == "" || name == "" {
200+
return
201+
}
202+
collaborators, err := FetchRepoCollaborators(ctx, client, owner, name)
203+
if err != nil {
204+
return
205+
}
206+
readerSets = append(readerSets, collaborators)
207+
}
208+
209+
label, ok := ifc.LabelSearchIssues(visibilities, readerSets)
210+
if !ok {
211+
return
212+
}
213+
if callResult.Meta == nil {
214+
callResult.Meta = mcp.Meta{}
215+
}
216+
callResult.Meta["ifc"] = label
217+
}
218+
169219
// SearchCode creates a tool to search for code across GitHub repositories.
170220
func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
171221
schema := &jsonschema.Schema{

pkg/github/search_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,187 @@ func Test_SearchRepositories(t *testing.T) {
163163
assert.Equal(t, *tc.expectedResult.Repositories[i].FullName, repo.FullName)
164164
assert.Equal(t, *tc.expectedResult.Repositories[i].HTMLURL, repo.HTMLURL)
165165
}
166+
})
167+
}
168+
}
169+
170+
func Test_SearchRepositories_IFC_InsidersMode(t *testing.T) {
171+
t.Parallel()
166172

173+
serverTool := SearchRepositories(translations.NullTranslationHelper)
174+
175+
type repoFixture struct {
176+
owner string
177+
name string
178+
isPrivate bool
179+
collaborators []string
180+
collaboratorsStatus int
181+
}
182+
183+
makeRepo := func(r repoFixture) *github.Repository {
184+
return &github.Repository{
185+
ID: github.Ptr(int64(1)),
186+
Name: github.Ptr(r.name),
187+
FullName: github.Ptr(r.owner + "/" + r.name),
188+
Private: github.Ptr(r.isPrivate),
189+
Owner: &github.User{Login: github.Ptr(r.owner)},
190+
}
191+
}
192+
193+
makeMockClient := func(repos []repoFixture) *http.Client {
194+
searchResult := &github.RepositoriesSearchResult{
195+
Total: github.Ptr(len(repos)),
196+
IncompleteResults: github.Ptr(false),