@@ -408,6 +408,24 @@ func mockToolWithMeta(name string, toolsetName string, readOnly bool) ServerTool
408408 }
409409}
410410
411+ // mockToolWithScopes creates a ServerTool with metadata including required scopes
412+ func mockToolWithScopes (name string , toolsetName string , readOnly bool , requiredScopes []string ) ServerTool {
413+ meta := mcp.Meta {"toolset" : toolsetName }
414+ if requiredScopes != nil {
415+ meta ["requiredOAuthScopes" ] = requiredScopes
416+ }
417+ return ServerTool {
418+ Tool : mcp.Tool {
419+ Name : name ,
420+ Meta : meta ,
421+ Annotations : & mcp.ToolAnnotations {
422+ ReadOnlyHint : readOnly ,
423+ },
424+ },
425+ RegisterFunc : func (_ * mcp.Server ) {},
426+ }
427+ }
428+
411429func TestNewToolsetGroupFromTools (t * testing.T ) {
412430 toolsetMetadatas := []ToolsetMetadata {
413431 {ID : "repos" , Description : "Repository tools" },
@@ -599,3 +617,196 @@ func TestGetToolsetFromMeta(t *testing.T) {
599617 })
600618 }
601619}
620+
621+ func TestToolsetRegistry_NewToolsetGroup (t * testing.T ) {
622+ toolsetMetadatas := []ToolsetMetadata {
623+ {ID : "repos" , Description : "Repository tools" },
624+ {ID : "issues" , Description : "Issue tools" },
625+ }
626+
627+ tools := []ServerTool {
628+ mockToolWithMeta ("get_repo" , "repos" , true ),
629+ mockToolWithMeta ("create_repo" , "repos" , false ),
630+ mockToolWithMeta ("get_issue" , "issues" , true ),
631+ mockToolWithMeta ("create_issue" , "issues" , false ),
632+ }
633+
634+ registry := NewToolsetRegistry (toolsetMetadatas , tools )
635+
636+ t .Run ("basic creation" , func (t * testing.T ) {
637+ tsg := registry .NewToolsetGroup (ToolsetGroupConfig {})
638+
639+ if len (tsg .Toolsets ) != 2 {
640+ t .Fatalf ("expected 2 toolsets, got %d" , len (tsg .Toolsets ))
641+ }
642+
643+ // Verify toolsets are not enabled by default
644+ if tsg .IsEnabled ("repos" ) {
645+ t .Error ("expected repos to be disabled by default" )
646+ }
647+ })
648+
649+ t .Run ("with active toolsets" , func (t * testing.T ) {
650+ tsg := registry .NewToolsetGroup (ToolsetGroupConfig {
651+ ActiveToolsets : []string {"repos" },
652+ })
653+
654+ if ! tsg .IsEnabled ("repos" ) {
655+ t .Error ("expected repos to be enabled" )
656+ }
657+ if tsg .IsEnabled ("issues" ) {
658+ t .Error ("expected issues to be disabled" )
659+ }
660+ })
661+
662+ t .Run ("with read-only mode" , func (t * testing.T ) {
663+ tsg := registry .NewToolsetGroup (ToolsetGroupConfig {
664+ ReadOnly : true ,
665+ ActiveToolsets : []string {"repos" },
666+ })
667+
668+ reposToolset := tsg .Toolsets ["repos" ]
669+ if ! reposToolset .readOnly {
670+ t .Error ("expected toolset to be in read-only mode" )
671+ }
672+
673+ activeTools := reposToolset .GetActiveTools ()
674+ if len (activeTools ) != 1 {
675+ t .Errorf ("expected 1 active tool in read-only mode, got %d" , len (activeTools ))
676+ }
677+ })
678+ }
679+
680+ func TestToolsetRegistry_NewToolsetGroup_WithScopes (t * testing.T ) {
681+ toolsetMetadatas := []ToolsetMetadata {
682+ {ID : "repos" , Description : "Repository tools" },
683+ {ID : "issues" , Description : "Issue tools" },
684+ }
685+
686+ tools := []ServerTool {
687+ mockToolWithScopes ("get_repo" , "repos" , true , nil ), // No scope required
688+ mockToolWithScopes ("create_repo" , "repos" , false , []string {"repo" }), // Requires repo scope
689+ mockToolWithScopes ("get_issue" , "issues" , true , []string {"repo" }), // Requires repo scope
690+ mockToolWithScopes ("public_issue" , "issues" , true , []string {}), // Empty scopes (no requirement)
691+ }
692+
693+ registry := NewToolsetRegistry (toolsetMetadatas , tools )
694+
695+ t .Run ("nil scopes allows all tools" , func (t * testing.T ) {
696+ tsg := registry .NewToolsetGroup (ToolsetGroupConfig {
697+ AvailableScopes : nil ,
698+ })
699+
700+ reposToolset := tsg .Toolsets ["repos" ]
701+ if len (reposToolset .readTools )+ len (reposToolset .writeTools ) != 2 {
702+ t .Errorf ("expected 2 tools in repos, got %d" , len (reposToolset .readTools )+ len (reposToolset .writeTools ))
703+ }
704+ })
705+
706+ t .Run ("empty scopes filters tools requiring scopes" , func (t * testing.T ) {
707+ tsg := registry .NewToolsetGroup (ToolsetGroupConfig {
708+ AvailableScopes : []string {}, // No scopes available
709+ })
710+
711+ // repos should only have get_repo (no scope required)
712+ reposToolset , exists := tsg .Toolsets ["repos" ]
713+ if ! exists {
714+ t .Fatal ("expected repos toolset to exist" )
715+ }
716+ if len (reposToolset .readTools ) != 1 {
717+ t .Errorf ("expected 1 read tool in repos (no scope required), got %d" , len (reposToolset .readTools ))
718+ }
719+ if len (reposToolset .writeTools ) != 0 {
720+ t .Errorf ("expected 0 write tools in repos (scope required), got %d" , len (reposToolset .writeTools ))
721+ }
722+
723+ // issues should only have public_issue (empty scopes)
724+ issuesToolset , exists := tsg .Toolsets ["issues" ]
725+ if ! exists {
726+ t .Fatal ("expected issues toolset to exist" )
727+ }
728+ if len (issuesToolset .readTools ) != 1 {
729+ t .Errorf ("expected 1 read tool in issues, got %d" , len (issuesToolset .readTools ))
730+ }
731+ })
732+
733+ t .Run ("with repo scope allows repo-scoped tools" , func (t * testing.T ) {
734+ tsg := registry .NewToolsetGroup (ToolsetGroupConfig {
735+ AvailableScopes : []string {"repo" },
736+ })
737+
738+ reposToolset := tsg .Toolsets ["repos" ]
739+ if len (reposToolset .readTools ) != 1 {
740+ t .Errorf ("expected 1 read tool, got %d" , len (reposToolset .readTools ))
741+ }
742+ if len (reposToolset .writeTools ) != 1 {
743+ t .Errorf ("expected 1 write tool, got %d" , len (reposToolset .writeTools ))
744+ }
745+
746+ issuesToolset := tsg .Toolsets ["issues" ]
747+ if len (issuesToolset .readTools ) != 2 {
748+ t .Errorf ("expected 2 read tools in issues, got %d" , len (issuesToolset .readTools ))
749+ }
750+ })
751+ }
752+
753+ func TestToolScopeSatisfied (t * testing.T ) {
754+ tests := []struct {
755+ name string
756+ tool ServerTool
757+ availableScopes []string
758+ expected bool
759+ }{
760+ {
761+ name : "no meta" ,
762+ tool : ServerTool {Tool : mcp.Tool {Meta : nil }},
763+ availableScopes : []string {},
764+ expected : true ,
765+ },
766+ {
767+ name : "no scope requirement" ,
768+ tool : mockToolWithScopes ("test" , "repos" , true , nil ),
769+ availableScopes : []string {},
770+ expected : true ,
771+ },
772+ {
773+ name : "empty scope requirement" ,
774+ tool : mockToolWithScopes ("test" , "repos" , true , []string {}),
775+ availableScopes : []string {},
776+ expected : true ,
777+ },
778+ {
779+ name : "scope required and available" ,
780+ tool : mockToolWithScopes ("test" , "repos" , true , []string {"repo" }),
781+ availableScopes : []string {"repo" },
782+ expected : true ,
783+ },
784+ {
785+ name : "scope required but not available" ,
786+ tool : mockToolWithScopes ("test" , "repos" , true , []string {"repo" }),
787+ availableScopes : []string {"gist" },
788+ expected : false ,
789+ },
790+ {
791+ name : "multiple scopes required all available" ,
792+ tool : mockToolWithScopes ("test" , "repos" , true , []string {"repo" , "gist" }),
793+ availableScopes : []string {"repo" , "gist" , "user" },
794+ expected : true ,
795+ },
796+ {
797+ name : "multiple scopes required one missing" ,
798+ tool : mockToolWithScopes ("test" , "repos" , true , []string {"repo" , "admin:org" }),
799+ availableScopes : []string {"repo" },
800+ expected : false ,
801+ },
802+ }
803+
804+ for _ , tt := range tests {
805+ t .Run (tt .name , func (t * testing.T ) {
806+ result := toolScopeSatisfied (tt .tool , tt .availableScopes )
807+ if result != tt .expected {
808+ t .Errorf ("expected %v, got %v" , tt .expected , result )
809+ }
810+ })
811+ }
812+ }
0 commit comments