package github import ( "context" "testing" "github.com/github/github-mcp-server/pkg/inventory" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCreateToolScopeFilter(t *testing.T) { // Create test tools with various scope requirements toolNoScopes := &inventory.ServerTool{ Tool: mcp.Tool{Name: "no_scopes_tool"}, AcceptedScopes: nil, } toolEmptyScopes := &inventory.ServerTool{ Tool: mcp.Tool{Name: "empty_scopes_tool"}, AcceptedScopes: []string{}, } toolRepoScope := &inventory.ServerTool{ Tool: mcp.Tool{Name: "repo_tool"}, AcceptedScopes: []string{"repo"}, } toolRepoScopeReadOnly := &inventory.ServerTool{ Tool: mcp.Tool{ Name: "repo_tool_readonly", Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true}, }, AcceptedScopes: []string{"repo"}, } toolPublicRepoScope := &inventory.ServerTool{ Tool: mcp.Tool{Name: "public_repo_tool"}, AcceptedScopes: []string{"public_repo", "repo"}, // repo is parent, also accepted } toolPublicRepoScopeReadOnly := &inventory.ServerTool{ Tool: mcp.Tool{ Name: "public_repo_tool_readonly", Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true}, }, AcceptedScopes: []string{"public_repo", "repo"}, } toolGistScope := &inventory.ServerTool{ Tool: mcp.Tool{Name: "gist_tool"}, AcceptedScopes: []string{"gist"}, } toolMultiScope := &inventory.ServerTool{ Tool: mcp.Tool{Name: "multi_scope_tool"}, AcceptedScopes: []string{"repo", "admin:org"}, } tests := []struct { name string tokenScopes []string tool *inventory.ServerTool expected bool }{ { name: "tool with no scopes is always visible", tokenScopes: []string{}, tool: toolNoScopes, expected: true, }, { name: "tool with empty scopes is always visible", tokenScopes: []string{"repo"}, tool: toolEmptyScopes, expected: true, }, { name: "token with exact scope can see tool", tokenScopes: []string{"repo"}, tool: toolRepoScope, expected: true, }, { name: "token with parent scope can see child-scoped tool", tokenScopes: []string{"repo"}, tool: toolPublicRepoScope, expected: true, }, { name: "token missing required scope cannot see tool", tokenScopes: []string{"gist"}, tool: toolRepoScope, expected: false, }, { name: "token with unrelated scope cannot see tool", tokenScopes: []string{"repo"}, tool: toolGistScope, expected: false, }, { name: "token with one of multiple accepted scopes can see tool", tokenScopes: []string{"admin:org"}, tool: toolMultiScope, expected: true, }, { name: "empty token scopes cannot see scoped tools", tokenScopes: []string{}, tool: toolRepoScope, expected: false, }, { name: "empty token scopes CAN see read-only repo tools (public repos)", tokenScopes: []string{}, tool: toolRepoScopeReadOnly, expected: true, }, { name: "empty token scopes CAN see read-only public_repo tools", tokenScopes: []string{}, tool: toolPublicRepoScopeReadOnly, expected: true, }, { name: "token with multiple scopes where one matches", tokenScopes: []string{"gist", "repo"}, tool: toolPublicRepoScope, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := CreateToolScopeFilter(tt.tokenScopes) result, err := filter(context.Background(), tt.tool) require.NoError(t, err) assert.Equal(t, tt.expected, result, "filter result should match expected") }) } } func TestCreateToolScopeFilter_Integration(t *testing.T) { // Test integration with inventory builder tools := []inventory.ServerTool{ { Tool: mcp.Tool{Name: "public_tool"}, Toolset: inventory.ToolsetMetadata{ID: "test"}, AcceptedScopes: nil, // No scopes required }, { Tool: mcp.Tool{Name: "repo_tool"}, Toolset: inventory.ToolsetMetadata{ID: "test"}, AcceptedScopes: []string{"repo"}, }, { Tool: mcp.Tool{Name: "gist_tool"}, Toolset: inventory.ToolsetMetadata{ID: "test"}, AcceptedScopes: []string{"gist"}, }, } // Create filter for token with only "repo" scope filter := CreateToolScopeFilter([]string{"repo"}) // Build inventory with the filter inv, err := inventory.NewBuilder(). SetTools(tools). WithToolsets([]string{"test"}). WithFilter(filter). Build() require.NoError(t, err) // Get available tools availableTools := inv.AvailableTools(context.Background()) // Should see public_tool and repo_tool, but not gist_tool assert.Len(t, availableTools, 2) toolNames := make([]string, len(availableTools)) for i, tool := range availableTools { toolNames[i] = tool.Tool.Name } assert.Contains(t, toolNames, "public_tool") assert.Contains(t, toolNames, "repo_tool") assert.NotContains(t, toolNames, "gist_tool") }