Skip to content

Commit 86d20eb

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, list_issues, get_file_contents, search_issues, and issue_read. Per the spec in github/copilot-mcp-core#1623, the label is PublicUntrusted() regardless of whether matched repositories are private. Repository search is treated as a discovery surface: matched repositories are by definition already accessible to the caller, so the joined readers are universal. Integrity is untrusted because repository names, descriptions, and topics are user-authored. Implementation note: SearchRepositories has its own handler distinct from the shared searchHandler used by SearchIssues, so the IFC attach is wired inline at the success return rather than via the postProcess hook added in #2456. Refs github/copilot-mcp-core#1623, github/copilot-mcp-core#1389. Note: chained on #2457 (gokhanarkan/fides-issue-read), which is in turn chained on #2456. GitHub will retarget the base to main once those merge.
1 parent 6181edd commit 86d20eb

3 files changed

Lines changed: 79 additions & 1 deletion

File tree

pkg/github/search.go

Lines changed: 9 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,7 +162,14 @@ 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+
if callResult.Meta == nil {
168+
callResult.Meta = mcp.Meta{}
169+
}
170+
callResult.Meta["ifc"] = ifc.LabelSearchRepositories()
171+
}
172+
return callResult, nil, nil
165173
},
166174
)
167175
}

pkg/github/search_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,67 @@ func Test_SearchRepositories_FullOutput(t *testing.T) {
228228
assert.Equal(t, *mockSearchResult.Repositories[0].Name, *returnedResult.Repositories[0].Name)
229229
}
230230

231+
func Test_SearchRepositories_IFC_InsidersMode(t *testing.T) {
232+
t.Parallel()
233+
234+
serverTool := SearchRepositories(translations.NullTranslationHelper)
235+
236+
mockSearchResult := &github.RepositoriesSearchResult{
237+
Total: github.Ptr(1),
238+
IncompleteResults: github.Ptr(false),
239+
Repositories: []*github.Repository{{
240+
ID: github.Ptr(int64(1)),
241+
Name: github.Ptr("repo"),
242+
FullName: github.Ptr("octocat/repo"),
243+
}},
244+
}
245+
246+
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
247+
GetSearchRepositories: mockResponse(t, http.StatusOK, mockSearchResult),
248+
})
249+
reqParams := map[string]any{"query": "octocat"}
250+
251+
t.Run("insiders mode disabled omits ifc label", func(t *testing.T) {
252+
deps := BaseDeps{
253+
Client: github.NewClient(mockedClient),
254+
Flags: FeatureFlags{InsidersMode: false},
255+
}
256+
handler := serverTool.Handler(deps)
257+
258+
request := createMCPRequest(reqParams)
259+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
260+
require.NoError(t, err)
261+
require.False(t, result.IsError)
262+
263+
assert.Nil(t, result.Meta)
264+
})
265+
266+
t.Run("insiders mode enabled emits public untrusted label", func(t *testing.T) {
267+
deps := BaseDeps{
268+
Client: github.NewClient(mockedClient),
269+
Flags: FeatureFlags{InsidersMode: true},
270+
}
271+
handler := serverTool.Handler(deps)
272+
273+
request := createMCPRequest(reqParams)
274+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
275+
require.NoError(t, err)
276+
require.False(t, result.IsError)
277+
278+
require.NotNil(t, result.Meta)
279+
ifcLabel, ok := result.Meta["ifc"]
280+
require.True(t, ok, "result meta should contain ifc key")
281+
282+
ifcJSON, err := json.Marshal(ifcLabel)
283+
require.NoError(t, err)
284+
var ifcMap map[string]any
285+
require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))
286+
287+
assert.Equal(t, "untrusted", ifcMap["integrity"])
288+
assert.Equal(t, []any{"public"}, ifcMap["confidentiality"])
289+
})
290+
}
291+
231292
func Test_SearchCode(t *testing.T) {
232293
// Verify tool definition once
233294
serverTool := SearchCode(translations.NullTranslationHelper)

pkg/ifc/ifc.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ func LabelGetFileContents(isPrivate bool, readers []string) SecurityLabel {
8787
return PublicUntrusted()
8888
}
8989

90+
// LabelSearchRepositories returns the IFC label for a search_repositories
91+
// result. Repository search is treated as a discovery surface: matched
92+
// repositories are by definition already accessible to the caller through
93+
// other means, so the joined readers are universal. Integrity is untrusted
94+
// because repository names, descriptions, and topics are user-authored.
95+
func LabelSearchRepositories() SecurityLabel {
96+
return PublicUntrusted()
97+
}
98+
9099
// LabelSearchIssues returns the IFC label for a search_issues result, joining
91100
// per-repository labels across all matched repositories.
92101
//

0 commit comments

Comments
 (0)