Skip to content

Commit 7a78ec6

Browse files
Add validation tests for tools, resources, and prompts metadata
This commit adds comprehensive validation tests to ensure all MCP items have required metadata: - TestAllToolsHaveRequiredMetadata: Validates Toolset.ID and Annotations - TestAllToolsHaveValidToolsetID: Ensures toolsets are in AvailableToolsets() - TestAllResourcesHaveRequiredMetadata: Validates resource metadata - TestAllPromptsHaveRequiredMetadata: Validates prompt metadata - TestToolReadOnlyHintConsistency: Validates IsReadOnly() matches annotation - TestNoDuplicate*Names: Ensures unique names across tools/resources/prompts - TestAllToolsHaveHandlerFunc: Ensures all tools have handlers - TestDefaultToolsetsAreValid: Validates default toolset IDs - TestToolsetMetadataConsistency: Ensures consistent descriptions per toolset Also fixes a bug discovered by these tests: ToolsetMetadataGit was defined but not added to AvailableToolsets(), causing get_repository_tree to have an invalid toolset ID.
1 parent ff237aa commit 7a78ec6

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func AvailableToolsets() []toolsets.ToolsetMetadata {
111111
return []toolsets.ToolsetMetadata{
112112
ToolsetMetadataContext,
113113
ToolsetMetadataRepos,
114+
ToolsetMetadataGit,
114115
ToolsetMetadataIssues,
115116
ToolsetMetadataPullRequests,
116117
ToolsetMetadataUsers,
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package github
2+
3+
import (
4+
"testing"
5+
6+
"github.com/github/github-mcp-server/pkg/toolsets"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// stubTranslation is a simple translation function for testing
12+
func stubTranslation(_, fallback string) string {
13+
return fallback
14+
}
15+
16+
// TestAllToolsHaveRequiredMetadata validates that all tools have mandatory metadata:
17+
// - Toolset must be set (non-empty ID)
18+
// - ReadOnlyHint annotation must be explicitly set (not nil)
19+
func TestAllToolsHaveRequiredMetadata(t *testing.T) {
20+
tools := AllTools(stubTranslation)
21+
22+
require.NotEmpty(t, tools, "AllTools should return at least one tool")
23+
24+
for _, tool := range tools {
25+
t.Run(tool.Tool.Name, func(t *testing.T) {
26+
// Toolset ID must be set
27+
assert.NotEmpty(t, tool.Toolset.ID,
28+
"Tool %q must have a Toolset.ID", tool.Tool.Name)
29+
30+
// Toolset description should be set for documentation
31+
assert.NotEmpty(t, tool.Toolset.Description,
32+
"Tool %q should have a Toolset.Description", tool.Tool.Name)
33+
34+
// Annotations must exist and have ReadOnlyHint explicitly set
35+
require.NotNil(t, tool.Tool.Annotations,
36+
"Tool %q must have Annotations set (for ReadOnlyHint)", tool.Tool.Name)
37+
38+
// We can't distinguish between "not set" and "set to false" for a bool,
39+
// but having Annotations non-nil confirms the developer thought about it.
40+
// The ReadOnlyHint value itself is validated by ensuring Annotations exist.
41+
})
42+
}
43+
}
44+
45+
// TestAllToolsHaveValidToolsetID validates that all tools belong to known toolsets
46+
func TestAllToolsHaveValidToolsetID(t *testing.T) {
47+
tools := AllTools(stubTranslation)
48+
validToolsetIDs := GetValidToolsetIDs()
49+
50+
for _, tool := range tools {
51+
t.Run(tool.Tool.Name, func(t *testing.T) {
52+
assert.True(t, validToolsetIDs[tool.Toolset.ID],
53+
"Tool %q has invalid Toolset.ID %q - must be one of the defined toolsets",
54+
tool.Tool.Name, tool.Toolset.ID)
55+
})
56+
}
57+
}
58+
59+
// TestAllResourcesHaveRequiredMetadata validates that all resources have mandatory metadata
60+
func TestAllResourcesHaveRequiredMetadata(t *testing.T) {
61+
// Resources need client functions, but we can pass nil for validation
62+
// since we're not actually calling handlers
63+
resources := AllResources(stubTranslation, nil, nil)
64+
65+
require.NotEmpty(t, resources, "AllResources should return at least one resource")
66+
67+
for _, res := range resources {
68+
t.Run(res.Template.Name, func(t *testing.T) {
69+
// Toolset ID must be set
70+
assert.NotEmpty(t, res.Toolset.ID,
71+
"Resource %q must have a Toolset.ID", res.Template.Name)
72+
73+
// Handler must be set
74+
assert.NotNil(t, res.Handler,
75+
"Resource %q must have a Handler", res.Template.Name)
76+
})
77+
}
78+
}
79+
80+
// TestAllPromptsHaveRequiredMetadata validates that all prompts have mandatory metadata
81+
func TestAllPromptsHaveRequiredMetadata(t *testing.T) {
82+
prompts := AllPrompts(stubTranslation)
83+
84+
require.NotEmpty(t, prompts, "AllPrompts should return at least one prompt")
85+
86+
for _, prompt := range prompts {
87+
t.Run(prompt.Prompt.Name, func(t *testing.T) {
88+
// Toolset ID must be set
89+
assert.NotEmpty(t, prompt.Toolset.ID,
90+
"Prompt %q must have a Toolset.ID", prompt.Prompt.Name)
91+
92+
// Handler must be set
93+
assert.NotNil(t, prompt.Handler,
94+
"Prompt %q must have a Handler", prompt.Prompt.Name)
95+
})
96+
}
97+
}
98+
99+
// TestAllResourcesHaveValidToolsetID validates that all resources belong to known toolsets
100+
func TestAllResourcesHaveValidToolsetID(t *testing.T) {
101+
resources := AllResources(stubTranslation, nil, nil)
102+
validToolsetIDs := GetValidToolsetIDs()
103+
104+
for _, res := range resources {
105+
t.Run(res.Template.Name, func(t *testing.T) {
106+
assert.True(t, validToolsetIDs[res.Toolset.ID],
107+
"Resource %q has invalid Toolset.ID %q", res.Template.Name, res.Toolset.ID)
108+
})
109+
}
110+
}
111+
112+
// TestAllPromptsHaveValidToolsetID validates that all prompts belong to known toolsets
113+
func TestAllPromptsHaveValidToolsetID(t *testing.T) {
114+
prompts := AllPrompts(stubTranslation)
115+
validToolsetIDs := GetValidToolsetIDs()
116+
117+
for _, prompt := range prompts {
118+
t.Run(prompt.Prompt.Name, func(t *testing.T) {
119+
assert.True(t, validToolsetIDs[prompt.Toolset.ID],
120+
"Prompt %q has invalid Toolset.ID %q", prompt.Prompt.Name, prompt.Toolset.ID)
121+
})
122+
}
123+
}
124+
125+
// TestToolReadOnlyHintConsistency validates that read-only tools are correctly annotated
126+
func TestToolReadOnlyHintConsistency(t *testing.T) {
127+
tools := AllTools(stubTranslation)
128+
129+
for _, tool := range tools {
130+
t.Run(tool.Tool.Name, func(t *testing.T) {
131+
require.NotNil(t, tool.Tool.Annotations,
132+
"Tool %q must have Annotations", tool.Tool.Name)
133+
134+
// Verify IsReadOnly() method matches the annotation
135+
assert.Equal(t, tool.Tool.Annotations.ReadOnlyHint, tool.IsReadOnly(),
136+
"Tool %q: IsReadOnly() should match Annotations.ReadOnlyHint", tool.Tool.Name)
137+
})
138+
}
139+
}
140+
141+
// TestNoDuplicateToolNames ensures all tools have unique names
142+
func TestNoDuplicateToolNames(t *testing.T) {
143+
tools := AllTools(stubTranslation)
144+
seen := make(map[string]bool)
145+
146+
for _, tool := range tools {
147+
name := tool.Tool.Name
148+
assert.False(t, seen[name],
149+
"Duplicate tool name found: %q", name)
150+
seen[name] = true
151+
}
152+
}
153+
154+
// TestNoDuplicateResourceNames ensures all resources have unique names
155+
func TestNoDuplicateResourceNames(t *testing.T) {
156+
resources := AllResources(stubTranslation, nil, nil)
157+
seen := make(map[string]bool)
158+
159+
for _, res := range resources {
160+
name := res.Template.Name
161+
assert.False(t, seen[name],
162+
"Duplicate resource name found: %q", name)
163+
seen[name] = true
164+
}
165+
}
166+
167+
// TestNoDuplicatePromptNames ensures all prompts have unique names
168+
func TestNoDuplicatePromptNames(t *testing.T) {
169+
prompts := AllPrompts(stubTranslation)
170+
seen := make(map[string]bool)
171+
172+
for _, prompt := range prompts {
173+
name := prompt.Prompt.Name
174+
assert.False(t, seen[name],
175+
"Duplicate prompt name found: %q", name)
176+
seen[name] = true
177+
}
178+
}
179+
180+
// TestAllToolsHaveHandlerFunc ensures all tools have a handler function
181+
func TestAllToolsHaveHandlerFunc(t *testing.T) {
182+
tools := AllTools(stubTranslation)
183+
184+
for _, tool := range tools {
185+
t.Run(tool.Tool.Name, func(t *testing.T) {
186+
assert.NotNil(t, tool.HandlerFunc,
187+
"Tool %q must have a HandlerFunc", tool.Tool.Name)
188+
})
189+
}
190+
}
191+
192+
// TestDefaultToolsetsAreValid ensures default toolset IDs are all valid
193+
func TestDefaultToolsetsAreValid(t *testing.T) {
194+
defaults := GetDefaultToolsetIDs()
195+
valid := GetValidToolsetIDs()
196+
197+
for _, id := range defaults {
198+
assert.True(t, valid[id],
199+
"Default toolset ID %q is not in the valid toolset list", id)
200+
}
201+
}
202+
203+
// TestToolsetMetadataConsistency ensures tools in the same toolset have consistent descriptions
204+
func TestToolsetMetadataConsistency(t *testing.T) {
205+
tools := AllTools(stubTranslation)
206+
toolsetDescriptions := make(map[toolsets.ToolsetID]string)
207+
208+
for _, tool := range tools {
209+
id := tool.Toolset.ID
210+
desc := tool.Toolset.Description
211+
212+
if existing, ok := toolsetDescriptions[id]; ok {
213+
assert.Equal(t, existing, desc,
214+
"Toolset %q has inconsistent descriptions across tools", id)
215+
} else {
216+
toolsetDescriptions[id] = desc
217+
}
218+
}
219+
}

0 commit comments

Comments
 (0)