Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion pkg/github/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/ifc"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
Expand Down Expand Up @@ -161,11 +162,60 @@ func SearchRepositories(t translations.TranslationHelperFunc) inventory.ServerTo
}
}

return utils.NewToolResultText(string(r)), nil, nil
callResult := utils.NewToolResultText(string(r))
if deps.GetFlags(ctx).InsidersMode {
attachSearchRepositoriesIFCLabel(ctx, client, result.Repositories, callResult)
}
return callResult, nil, nil
},
)
}

// attachSearchRepositoriesIFCLabel joins per-repository IFC labels across
// every matched repository and attaches the result to callResult. Visibility
// is read directly from the search response (no extra API call); collaborators
// are fetched once per private repository. If any collaborators lookup fails
// the label is omitted to avoid misclassifying the result. The join math is
// shared with search_issues via ifc.LabelSearchIssues: integrity is always
// untrusted, and confidentiality is the intersection of the reader sets of
// the matched private repositories (public matches contribute the universe
// set and drop out without shrinking it).
func attachSearchRepositoriesIFCLabel(ctx context.Context, client *github.Client, repos []*github.Repository, callResult *mcp.CallToolResult) {
if callResult == nil || callResult.IsError {
return
}

visibilities := make([]bool, 0, len(repos))
readerSets := make([][]string, 0, len(repos))
for _, repo := range repos {
isPrivate := repo.GetPrivate()
visibilities = append(visibilities, isPrivate)
if !isPrivate {
readerSets = append(readerSets, nil)
continue
}
owner := repo.GetOwner().GetLogin()
name := repo.GetName()
if owner == "" || name == "" {
return
}
collaborators, err := FetchRepoCollaborators(ctx, client, owner, name)
if err != nil {
return
}
readerSets = append(readerSets, collaborators)
}

label, ok := ifc.LabelSearchIssues(visibilities, readerSets)
if !ok {
return
}
if callResult.Meta == nil {
callResult.Meta = mcp.Meta{}
}
callResult.Meta["ifc"] = label
}

// SearchCode creates a tool to search for code across GitHub repositories.
func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
schema := &jsonschema.Schema{
Expand Down
178 changes: 178 additions & 0 deletions pkg/github/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,187 @@ func Test_SearchRepositories(t *testing.T) {
assert.Equal(t, *tc.expectedResult.Repositories[i].FullName, repo.FullName)
assert.Equal(t, *tc.expectedResult.Repositories[i].HTMLURL, repo.HTMLURL)
}
})
}
}

func Test_SearchRepositories_IFC_InsidersMode(t *testing.T) {
t.Parallel()

serverTool := SearchRepositories(translations.NullTranslationHelper)

type repoFixture struct {
owner string
name string
isPrivate bool
collaborators []string
collaboratorsStatus int
}

makeRepo := func(r repoFixture) *github.Repository {
return &github.Repository{
ID: github.Ptr(int64(1)),
Name: github.Ptr(r.name),
FullName: github.Ptr(r.owner + "/" + r.name),
Private: github.Ptr(r.isPrivate),
Owner: &github.User{Login: github.Ptr(r.owner)},
}
}

makeMockClient := func(repos []repoFixture) *http.Client {
searchResult := &github.RepositoriesSearchResult{
Total: github.Ptr(len(repos)),
IncompleteResults: github.Ptr(false),
}
for _, r := range repos {
searchResult.Repositories = append(searchResult.Repositories, makeRepo(r))
}

collaboratorsByPath := map[string]repoFixture{}
for _, r := range repos {
collaboratorsByPath["/repos/"+r.owner+"/"+r.name+"/collaborators"] = r
}

return MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetSearchRepositories: mockResponse(t, http.StatusOK, searchResult),
GetReposCollaboratorsByOwnerByRepo: func(w http.ResponseWriter, req *http.Request) {
r, ok := collaboratorsByPath[req.URL.Path]
if !ok {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("[]"))
return
}
if r.collaboratorsStatus != 0 && r.collaboratorsStatus != http.StatusOK {
w.WriteHeader(r.collaboratorsStatus)
return
}
users := make([]*github.User, len(r.collaborators))
for i, login := range r.collaborators {
users[i] = &github.User{Login: github.Ptr(login)}
}
body, _ := json.Marshal(users)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
},
})
}

reqParams := map[string]any{"query": "octocat"}

t.Run("insiders mode disabled omits ifc label", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient([]repoFixture{{owner: "octocat", name: "public-repo"}})),
Flags: FeatureFlags{InsidersMode: false},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(reqParams)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)
assert.Nil(t, result.Meta)
})

t.Run("insiders mode all public emits public untrusted", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient([]repoFixture{
{owner: "octocat", name: "public-a"},
{owner: "octocat", name: "public-b"},
})),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(reqParams)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta)
ifcMap := unmarshalIFC(t, result.Meta["ifc"])
assert.Equal(t, "untrusted", ifcMap["integrity"])
assert.Equal(t, []any{"public"}, ifcMap["confidentiality"])
})

t.Run("insiders mode mixed public and private keeps the private readers", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient([]repoFixture{
{owner: "octocat", name: "private-repo", isPrivate: true, collaborators: []string{"alice"}},
{owner: "octocat", name: "public-repo"},
})),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(reqParams)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta)
ifcMap := unmarshalIFC(t, result.Meta["ifc"])
assert.Equal(t, "untrusted", ifcMap["integrity"])
assert.Equal(t, []any{"alice"}, ifcMap["confidentiality"])
})

t.Run("insiders mode two private repos intersect collaborators", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient([]repoFixture{
{owner: "octocat", name: "repo-a", isPrivate: true, collaborators: []string{"alice", "bob", "carol"}},
{owner: "octocat", name: "repo-b", isPrivate: true, collaborators: []string{"bob", "carol", "dan"}},
})),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(reqParams)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta)
ifcMap := unmarshalIFC(t, result.Meta["ifc"])
assert.Equal(t, "untrusted", ifcMap["integrity"])
assert.Equal(t, []any{"bob", "carol"}, ifcMap["confidentiality"])
})

t.Run("insiders mode skips ifc label when collaborators lookup fails", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient([]repoFixture{
{owner: "octocat", name: "private-repo", isPrivate: true, collaboratorsStatus: http.StatusInternalServerError},
})),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(reqParams)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError, "tool call should still succeed when collaborators lookup fails")

if result.Meta != nil {
_, hasIFC := result.Meta["ifc"]
assert.False(t, hasIFC, "ifc label should be omitted when collaborators lookup fails")
}
})

t.Run("insiders mode empty results emits public untrusted", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient(nil)),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(reqParams)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta)
ifcMap := unmarshalIFC(t, result.Meta["ifc"])
assert.Equal(t, "untrusted", ifcMap["integrity"])
assert.Equal(t, []any{"public"}, ifcMap["confidentiality"])
})
}

func Test_SearchRepositories_FullOutput(t *testing.T) {
Expand Down
Loading