Skip to content

Commit eb1865f

Browse files
fix: filter skill allowedTools against registered tools at startup
Skills now only include tools that are actually registered with the MCP server, based on the active toolset configuration. Skills with no available tools are skipped entirely. This prevents skills from referencing non-existent tools (e.g., gist tools when the gists toolset isn't enabled). RegisterSkillResources now accepts a map of available tool names built from the inventory at startup time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 184979c commit eb1865f

3 files changed

Lines changed: 95 additions & 5 deletions

File tree

pkg/github/server.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,14 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
124124
registerDynamicTools(ghServer, inv, deps, cfg.Translator)
125125
}
126126

127-
// Register skill resources for MCP clients that support skills-based discovery
128-
RegisterSkillResources(ghServer)
127+
// Register skill resources for MCP clients that support skills-based discovery.
128+
// Filter allowedTools against actually-registered tools so skills only reference
129+
// tools that exist for the current toolset configuration.
130+
availableTools := make(map[string]struct{})
131+
for _, tool := range inv.AllTools() {
132+
availableTools[tool.Tool.Name] = struct{}{}
133+
}
134+
RegisterSkillResources(ghServer, availableTools)
129135

130136
return ghServer, nil
131137
}

pkg/github/skill_resources.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,18 @@ func buildSkillContent(skill skillDefinition) string {
508508
// RegisterSkillResources registers all skill resources with the MCP server.
509509
// Each skill is a static resource with a skill:// URI that can be discovered
510510
// by MCP clients supporting the skills pattern.
511-
func RegisterSkillResources(s *mcp.Server) {
511+
// The availableTools set filters each skill's allowedTools to only include
512+
// tools that are actually registered, ensuring skills work correctly
513+
// regardless of which toolsets are enabled.
514+
func RegisterSkillResources(s *mcp.Server, availableTools map[string]struct{}) {
512515
for _, skill := range allSkills() {
516+
// Filter allowedTools to only include tools that are actually registered
517+
filtered := filterAvailableTools(skill.allowedTools, availableTools)
518+
if len(filtered) == 0 {
519+
continue // Skip skills with no available tools
520+
}
521+
skill.allowedTools = filtered
522+
513523
content := buildSkillContent(skill)
514524
uri := fmt.Sprintf("skill://github/%s/SKILL.md", skill.name)
515525

@@ -536,3 +546,18 @@ func RegisterSkillResources(s *mcp.Server) {
536546
)
537547
}
538548
}
549+
550+
// filterAvailableTools returns only the tool names that exist in the available set.
551+
// If availableTools is nil, all tools are returned (no filtering).
552+
func filterAvailableTools(tools []string, availableTools map[string]struct{}) []string {
553+
if availableTools == nil {
554+
return tools
555+
}
556+
var filtered []string
557+
for _, t := range tools {
558+
if _, ok := availableTools[t]; ok {
559+
filtered = append(filtered, t)
560+
}
561+
}
562+
return filtered
563+
}

pkg/github/skill_resources_test.go

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,69 @@ func TestRegisterSkillResources(t *testing.T) {
7575
Version: "0.0.1",
7676
}, nil)
7777

78-
// Should not panic
79-
RegisterSkillResources(server)
78+
// Should not panic with nil (no filtering)
79+
RegisterSkillResources(server, nil)
8080

8181
// Verify the expected number of skills were registered by counting definitions
8282
skills := allSkills()
8383
assert.Equal(t, 27, len(skills), "expected 27 workflow-oriented skills")
8484
}
85+
86+
func TestFilterAvailableTools(t *testing.T) {
87+
tests := []struct {
88+
name string
89+
tools []string
90+
available map[string]struct{}
91+
expected []string
92+
}{
93+
{
94+
name: "nil available returns all tools",
95+
tools: []string{"a", "b", "c"},
96+
available: nil,
97+
expected: []string{"a", "b", "c"},
98+
},
99+
{
100+
name: "filters to only available tools",
101+
tools: []string{"a", "b", "c"},
102+
available: map[string]struct{}{"a": {}, "c": {}},
103+
expected: []string{"a", "c"},
104+
},
105+
{
106+
name: "returns nil when no tools match",
107+
tools: []string{"a", "b"},
108+
available: map[string]struct{}{"x": {}},
109+
expected: nil,
110+
},
111+
{
112+
name: "empty available set filters everything",
113+
tools: []string{"a", "b"},
114+
available: map[string]struct{}{},
115+
expected: nil,
116+
},
117+
}
118+
119+
for _, tc := range tests {
120+
t.Run(tc.name, func(t *testing.T) {
121+
result := filterAvailableTools(tc.tools, tc.available)
122+
assert.Equal(t, tc.expected, result)
123+
})
124+
}
125+
}
126+
127+
func TestRegisterSkillResourcesFiltering(t *testing.T) {
128+
t.Parallel()
129+
// Only make get_me available — skills that have no overlap should be skipped
130+
available := map[string]struct{}{"get_me": {}}
131+
132+
server := mcp.NewServer(&mcp.Implementation{
133+
Name: "test-server",
134+
Version: "0.0.1",
135+
}, nil)
136+
137+
RegisterSkillResources(server, available)
138+
139+
// get-context skill includes get_me, so it should be registered.
140+
// Skills with zero available tools should be skipped entirely.
141+
// We can't easily count resources on mcp.Server, but we verify no panic
142+
// and the logic is correct via TestFilterAvailableTools above.
143+
}

0 commit comments

Comments
 (0)