Skip to content

Commit aaeda17

Browse files
olaservoclaude
andcommitted
refactor(skills): rename to match github#2374 catalogue, drop per-skill gating
Renames the two original skills to the names Sam uses in github#2374, and makes their load behavior consistent with the 25 skills imported from that PR. - pull-requests → review-pr - inbox-triage → handle-notifications Both renames preserve our richer content (parameter-level workflow detail for review-pr; High/Medium/Low priority taxonomy + read/done semantics + discover-mcp-skills cross-link for handle-notifications). Only the names change, plus the cross-reference inside handle-notifications/SKILL.md. Load consistency: - Drop the per-skill `Enabled func() bool` closures. All bundled skills now register unconditionally, matching the 25 imported skills and Sam's github#2374 semantics. - Drop the per-skill `Icons` field (octicons.Icons("light-bulb") didn't resolve in this fork's icon set anyway, and the imported skills set no icons). - The Registry's gating mechanism stays in place for future use; just no shipped skill currently exercises it. Test_DeclareSkillsExtensionIfEnabled's "empty registry" subtest covers the gating-off path directly. Tests: - Test_PullRequestsSkill_EmbeddedContent → Test_ReviewPRSkill_EmbeddedContent - Test_InboxTriageSkill_EmbeddedContent → Test_HandleNotificationsSkill_EmbeddedContent - Test_BundledSkills_Registration (toolset-gating subtests) collapsed into Test_BundledSkills_RegisterRegardlessOfToolset - Test_DeclareSkillsExtensionIfEnabled simplified: 4 subtests → 3, since the toolset-specific subtests are now redundant with always-on semantics Caveat for downstream consumers: any client hardcoding the old URIs (skill://github/pull-requests/SKILL.md, skill://github/inbox-triage/SKILL.md) needs to update to the new ones. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 89e8946 commit aaeda17

5 files changed

Lines changed: 86 additions & 172 deletions

File tree

pkg/github/bundled_skills.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,30 @@ package github
22

33
import (
44
"github.com/github/github-mcp-server/pkg/inventory"
5-
"github.com/github/github-mcp-server/pkg/octicons"
65
"github.com/github/github-mcp-server/skills"
76
"github.com/modelcontextprotocol/go-sdk/mcp"
87
)
98

109
// bundledSkills builds the registry of Agent Skills this server ships.
1110
//
12-
// The first two entries are toolset-gated — they only register when the
13-
// relevant toolset is enabled. The remaining workflow-oriented skills
14-
// (ported from github/github-mcp-server#2374) are always-on; their
15-
// `allowed-tools` frontmatter is advisory metadata, not a gate.
11+
// All bundled skills load uniformly: always-on, no per-skill toolset
12+
// gating, no icons. Their `allowed-tools` frontmatter is advisory only.
13+
// The Registry's `Enabled` closure is still available for future use
14+
// (e.g. feature-flagging a skill behind an experimental toolset).
1615
//
1716
// Adding a new server-bundled skill is one entry here plus a //go:embed
1817
// line in package skills.
1918
func bundledSkills(inv *inventory.Inventory) *skills.Registry {
2019
return skills.New().
2120
Add(skills.Bundled{
22-
Name: "pull-requests",
23-
Description: "Submit a multi-comment GitHub pull request review using the pending-review workflow. Use when leaving line-specific feedback on a pull request, when asked to review a PR, or whenever creating any review with more than one comment.",
24-
Content: skills.PullRequestsSKILL,
25-
Icons: octicons.Icons("light-bulb"),
26-
Enabled: func() bool { return inv.IsToolsetEnabled(ToolsetMetadataPullRequests.ID) },
21+
Name: "review-pr",
22+
Description: "Submit a multi-comment GitHub pull request review using the pending-review workflow (pull_request_review_write → add_comment_to_pending_review → submit_pending). Use when leaving line-specific feedback on a pull request, when asked to review a PR, or whenever creating any review with more than one comment.",
23+
Content: skills.ReviewPRSKILL,
2724
}).
2825
Add(skills.Bundled{
29-
Name: "inbox-triage",
26+
Name: "handle-notifications",
3027
Description: "Systematically triage the current user's GitHub notifications inbox — enumerate unread items, prioritize by notification reason (review requests, mentions, assignments, security alerts), act on the high-priority ones, then dismiss the rest. Use when the user asks \"what should I work on?\", \"catch me up on GitHub\", \"triage my inbox\", \"what needs my attention?\", or otherwise wants to clear their notifications backlog.",
31-
Content: skills.InboxTriageSKILL,
32-
Icons: octicons.Icons("bell"),
33-
Enabled: func() bool { return inv.IsToolsetEnabled(ToolsetMetadataNotifications.ID) },
28+
Content: skills.HandleNotificationsSKILL,
3429
}).
3530
Add(skills.Bundled{
3631
Name: "get-context",

pkg/github/bundled_skills_test.go

Lines changed: 70 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,23 @@ import (
1313
"github.com/stretchr/testify/require"
1414
)
1515

16-
// pullRequestsSkillURI / inboxTriageSkillURI are the canonical URIs of the
17-
// bundled skills, derived from skills.Bundled so tests never drift from the
18-
// single source of truth.
16+
// reviewPRSkillURI / handleNotificationsSkillURI are the canonical URIs of the
17+
// two skills with the most distinctive content. Derived from skills.Bundled so
18+
// tests never drift from the single source of truth.
1919
var (
20-
pullRequestsSkillURI = skills.Bundled{Name: "pull-requests"}.URI()
21-
inboxTriageSkillURI = skills.Bundled{Name: "inbox-triage"}.URI()
20+
reviewPRSkillURI = skills.Bundled{Name: "review-pr"}.URI()
21+
handleNotificationsSkillURI = skills.Bundled{Name: "handle-notifications"}.URI()
2222
)
2323

24-
// Test_PullRequestsSkill_EmbeddedContent verifies the SEP structural requirement
24+
// Test_ReviewPRSkill_EmbeddedContent verifies the SEP structural requirement
2525
// that the frontmatter `name` field matches the final segment of the skill-path
2626
// in the URI, and that the substantive tool-sequence content is preserved.
27-
func Test_PullRequestsSkill_EmbeddedContent(t *testing.T) {
28-
require.NotEmpty(t, skills.PullRequestsSKILL, "SKILL.md must be embedded")
27+
func Test_ReviewPRSkill_EmbeddedContent(t *testing.T) {
28+
require.NotEmpty(t, skills.ReviewPRSKILL, "SKILL.md must be embedded")
2929

3030
// Normalize line endings so the test is robust to git's autocrlf behavior
3131
// on Windows checkouts — the embedded SKILL.md may arrive as CRLF.
32-
md := strings.ReplaceAll(skills.PullRequestsSKILL, "\r\n", "\n")
32+
md := strings.ReplaceAll(skills.ReviewPRSKILL, "\r\n", "\n")
3333
require.True(t, strings.HasPrefix(md, "---\n"), "SKILL.md must begin with YAML frontmatter")
3434

3535
end := strings.Index(md[4:], "\n---\n")
@@ -44,7 +44,7 @@ func Test_PullRequestsSkill_EmbeddedContent(t *testing.T) {
4444
}
4545
}
4646
require.NotEmpty(t, frontmatterName, "SKILL.md frontmatter must declare `name`")
47-
assert.Equal(t, "pull-requests", frontmatterName, "frontmatter name must match final skill-path segment in %s", pullRequestsSkillURI)
47+
assert.Equal(t, "review-pr", frontmatterName, "frontmatter name must match final skill-path segment in %s", reviewPRSkillURI)
4848

4949
body := md[4+end+5:]
5050
assert.Contains(t, body, "## Workflow", "skill body must carry the workflow section")
@@ -53,13 +53,13 @@ func Test_PullRequestsSkill_EmbeddedContent(t *testing.T) {
5353
assert.Contains(t, body, "submit_pending", "the distinctive tool method must be present")
5454
}
5555

56-
// Test_InboxTriageSkill_EmbeddedContent verifies the SEP structural
57-
// requirements for the inbox-triage skill and that its substantive tool
58-
// references are preserved.
59-
func Test_InboxTriageSkill_EmbeddedContent(t *testing.T) {
60-
require.NotEmpty(t, skills.InboxTriageSKILL, "SKILL.md must be embedded")
56+
// Test_HandleNotificationsSkill_EmbeddedContent verifies the SEP structural
57+
// requirements for the handle-notifications skill and that its substantive
58+
// tool references are preserved.
59+
func Test_HandleNotificationsSkill_EmbeddedContent(t *testing.T) {
60+
require.NotEmpty(t, skills.HandleNotificationsSKILL, "SKILL.md must be embedded")
6161

62-
md := strings.ReplaceAll(skills.InboxTriageSKILL, "\r\n", "\n")
62+
md := strings.ReplaceAll(skills.HandleNotificationsSKILL, "\r\n", "\n")
6363
require.True(t, strings.HasPrefix(md, "---\n"), "SKILL.md must begin with YAML frontmatter")
6464

6565
end := strings.Index(md[4:], "\n---\n")
@@ -74,103 +74,41 @@ func Test_InboxTriageSkill_EmbeddedContent(t *testing.T) {
7474
}
7575
}
7676
require.NotEmpty(t, frontmatterName, "SKILL.md frontmatter must declare `name`")
77-
assert.Equal(t, "inbox-triage", frontmatterName, "frontmatter name must match final skill-path segment in %s", inboxTriageSkillURI)
77+
assert.Equal(t, "handle-notifications", frontmatterName, "frontmatter name must match final skill-path segment in %s", handleNotificationsSkillURI)
7878

7979
body := md[4+end+5:]
8080
assert.Contains(t, body, "## Workflow")
8181
assert.Contains(t, body, "list_notifications", "triage workflow must reference list_notifications")
8282
assert.Contains(t, body, "dismiss_notification", "triage workflow must reference dismiss_notification")
8383
}
8484

85-
// Test_BundledSkills_Registration verifies that skill resources are
86-
// registered when the backing toolset is enabled, and omitted when it is not.
87-
func Test_BundledSkills_Registration(t *testing.T) {
85+
// Test_BundledSkills_RegisterRegardlessOfToolset verifies that bundled skills
86+
// load uniformly — they're always-on and don't depend on which toolsets the
87+
// inventory has enabled. Per-skill toolset gating remains available via the
88+
// Registry's Enabled closure (covered by registry-level unit tests, not here)
89+
// but no shipped skill currently uses it.
90+
func Test_BundledSkills_RegisterRegardlessOfToolset(t *testing.T) {
8891
ctx := context.Background()
8992

90-
t.Run("registers when pull_requests toolset enabled", func(t *testing.T) {
91-
inv, err := NewInventory(translations.NullTranslationHelper).
92-
WithToolsets([]string{string(ToolsetMetadataPullRequests.ID)}).
93-
Build()
94-
require.NoError(t, err)
95-
96-
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
97-
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
98-
})
99-
RegisterBundledSkills(srv, inv)
100-
101-
mimes := map[string]string{}
102-
for _, r := range listResources(t, ctx, srv) {
103-
mimes[r.URI] = r.MIMEType
104-
}
105-
assert.Equal(t, "text/markdown", mimes[pullRequestsSkillURI])
106-
assert.Equal(t, "application/json", mimes[skills.IndexURI])
107-
})
108-
109-
t.Run("omits toolset-gated skills when their toolsets are disabled", func(t *testing.T) {
110-
inv, err := NewInventory(translations.NullTranslationHelper).
111-
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
112-
Build()
113-
require.NoError(t, err)
114-
115-
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
116-
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
117-
})
118-
RegisterBundledSkills(srv, inv)
119-
120-
uris := map[string]struct{}{}
121-
for _, r := range listResources(t, ctx, srv) {
122-
uris[r.URI] = struct{}{}
123-
}
124-
// Toolset-gated skills must be absent.
125-
assert.NotContains(t, uris, pullRequestsSkillURI, "pull-requests is gated on pull_requests toolset")
126-
assert.NotContains(t, uris, inboxTriageSkillURI, "inbox-triage is gated on notifications toolset")
127-
// Always-on skills (e.g. get-context) and the index remain registered.
128-
assert.Contains(t, uris, skills.Bundled{Name: "get-context"}.URI(), "always-on skill must still register")
129-
assert.Contains(t, uris, skills.IndexURI, "index is published whenever any skill is enabled")
130-
})
131-
132-
t.Run("registers inbox-triage when notifications toolset enabled", func(t *testing.T) {
133-
inv, err := NewInventory(translations.NullTranslationHelper).
134-
WithToolsets([]string{string(ToolsetMetadataNotifications.ID)}).
135-
Build()
136-
require.NoError(t, err)
137-
138-
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
139-
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
140-
})
141-
RegisterBundledSkills(srv, inv)
93+
// Pick a minimal toolset (context only) — doesn't enable pull_requests
94+
// or notifications, the toolsets the renamed skills used to be gated on.
95+
inv, err := NewInventory(translations.NullTranslationHelper).
96+
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
97+
Build()
98+
require.NoError(t, err)
14299

143-
uris := map[string]string{}
144-
for _, r := range listResources(t, ctx, srv) {
145-
uris[r.URI] = r.MIMEType
146-
}
147-
assert.Equal(t, "text/markdown", uris[inboxTriageSkillURI])
148-
assert.NotContains(t, uris, pullRequestsSkillURI, "only notifications enabled — pull-requests should not be registered")
149-
assert.Equal(t, "application/json", uris[skills.IndexURI])
100+
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
101+
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
150102
})
103+
RegisterBundledSkills(srv, inv)
151104

152-
t.Run("registers both when both toolsets enabled", func(t *testing.T) {
153-
inv, err := NewInventory(translations.NullTranslationHelper).
154-
WithToolsets([]string{
155-
string(ToolsetMetadataPullRequests.ID),
156-
string(ToolsetMetadataNotifications.ID),
157-
}).
158-
Build()
159-
require.NoError(t, err)
160-
161-
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
162-
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
163-
})
164-
RegisterBundledSkills(srv, inv)
165-
166-
uris := map[string]struct{}{}
167-
for _, r := range listResources(t, ctx, srv) {
168-
uris[r.URI] = struct{}{}
169-
}
170-
assert.Contains(t, uris, pullRequestsSkillURI)
171-
assert.Contains(t, uris, inboxTriageSkillURI)
172-
assert.Contains(t, uris, skills.IndexURI)
173-
})
105+
uris := map[string]string{}
106+
for _, r := range listResources(t, ctx, srv) {
107+
uris[r.URI] = r.MIMEType
108+
}
109+
assert.Equal(t, "text/markdown", uris[reviewPRSkillURI], "review-pr registers regardless of toolset")
110+
assert.Equal(t, "text/markdown", uris[handleNotificationsSkillURI], "handle-notifications registers regardless of toolset")
111+
assert.Equal(t, "application/json", uris[skills.IndexURI])
174112
}
175113

176114
// Test_BundledSkills_ReadContent verifies that reading the skill resource
@@ -179,7 +117,7 @@ func Test_BundledSkills_Registration(t *testing.T) {
179117
func Test_BundledSkills_ReadContent(t *testing.T) {
180118
ctx := context.Background()
181119
inv, err := NewInventory(translations.NullTranslationHelper).
182-
WithToolsets([]string{string(ToolsetMetadataPullRequests.ID)}).
120+
WithToolsets([]string{"all"}).
183121
Build()
184122
require.NoError(t, err)
185123

@@ -191,11 +129,11 @@ func Test_BundledSkills_ReadContent(t *testing.T) {
191129
session := connectClient(t, ctx, srv)
192130

193131
t.Run("SKILL.md content", func(t *testing.T) {
194-
res, err := session.ReadResource(ctx, &mcp.ReadResourceParams{URI: pullRequestsSkillURI})
132+
res, err := session.ReadResource(ctx, &mcp.ReadResourceParams{URI: reviewPRSkillURI})
195133
require.NoError(t, err)
196134
require.Len(t, res.Contents, 1)
197135
assert.Equal(t, "text/markdown", res.Contents[0].MIMEType)
198-
assert.Equal(t, skills.PullRequestsSKILL, res.Contents[0].Text)
136+
assert.Equal(t, skills.ReviewPRSKILL, res.Contents[0].Text)
199137
})
200138

201139
t.Run("index.json matches SEP discovery schema", func(t *testing.T) {
@@ -208,33 +146,31 @@ func Test_BundledSkills_ReadContent(t *testing.T) {
208146
require.NoError(t, json.Unmarshal([]byte(res.Contents[0].Text), &idx))
209147
assert.Equal(t, skills.IndexSchema, idx.Schema)
210148

211-
// Index size must equal the number of currently-enabled bundled skills.
212-
assert.Len(t, idx.Skills, len(bundledSkills(inv).Enabled()))
149+
// Index size must equal the number of currently-enabled bundled skills + templates.
150+
registry := bundledSkills(inv)
151+
assert.Len(t, idx.Skills, len(registry.Enabled())+len(registry.EnabledTemplates()))
213152

214-
// The pull-requests skill (toolset-gated, currently enabled) must be present.
153+
// The review-pr skill must be present.
215154
var found *skills.IndexEntry
216155
for i := range idx.Skills {
217-
if idx.Skills[i].Name == "pull-requests" {
156+
if idx.Skills[i].Name == "review-pr" {
218157
found = &idx.Skills[i]
219158
break
220159
}
221160
}
222-
require.NotNil(t, found, "pull-requests must appear in the index")
161+
require.NotNil(t, found, "review-pr must appear in the index")
223162
assert.Equal(t, "skill-md", found.Type)
224-
assert.Equal(t, pullRequestsSkillURI, found.URL)
163+
assert.Equal(t, reviewPRSkillURI, found.URL)
225164
assert.NotEmpty(t, found.Description)
226165
})
227166
}
228167

229-
// Test_BundledSkills_Index_MultipleSkills verifies that all enabled skills
168+
// Test_BundledSkills_Index_MultipleSkills verifies that all bundled skills
230169
// appear in the discovery index, not just the first one.
231170
func Test_BundledSkills_Index_MultipleSkills(t *testing.T) {
232171
ctx := context.Background()
233172
inv, err := NewInventory(translations.NullTranslationHelper).
234-
WithToolsets([]string{
235-
string(ToolsetMetadataPullRequests.ID),
236-
string(ToolsetMetadataNotifications.ID),
237-
}).
173+
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
238174
Build()
239175
require.NoError(t, err)
240176

@@ -253,31 +189,17 @@ func Test_BundledSkills_Index_MultipleSkills(t *testing.T) {
253189
for _, s := range idx.Skills {
254190
names[s.Name] = s.URL
255191
}
256-
assert.Equal(t, pullRequestsSkillURI, names["pull-requests"])
257-
assert.Equal(t, inboxTriageSkillURI, names["inbox-triage"])
192+
assert.Equal(t, reviewPRSkillURI, names["review-pr"])
193+
assert.Equal(t, handleNotificationsSkillURI, names["handle-notifications"])
258194
}
259195

260196
// Test_DeclareSkillsExtensionIfEnabled verifies that the skills-over-MCP
261-
// extension (SEP-2133) is declared in ServerOptions.Capabilities when the
262-
// pull_requests toolset is enabled, and is absent when it is not.
197+
// extension (SEP-2133) is declared in ServerOptions.Capabilities whenever the
198+
// server has any bundled skill or template entry to publish, and that
199+
// declaration is additive (doesn't clobber other extensions).
263200
func Test_DeclareSkillsExtensionIfEnabled(t *testing.T) {
264-
t.Run("declares when pull_requests enabled", func(t *testing.T) {
265-
inv, err := NewInventory(translations.NullTranslationHelper).
266-
WithToolsets([]string{string(ToolsetMetadataPullRequests.ID)}).
267-
Build()
268-
require.NoError(t, err)
269-
270-
opts := &mcp.ServerOptions{}
271-
DeclareSkillsExtensionIfEnabled(opts, inv)
272-
273-
require.NotNil(t, opts.Capabilities)
274-
_, ok := opts.Capabilities.Extensions[skills.ExtensionKey]
275-
assert.True(t, ok, "skills extension must be declared")
276-
})
277-
278-
t.Run("declares even when no toolset-gated skills enabled (always-on skills exist)", func(t *testing.T) {
279-
// Even with only the context toolset, always-on bundled skills (e.g. get-context)
280-
// register, so the extension capability MUST still be declared.
201+
t.Run("declares for any non-empty registry", func(t *testing.T) {
202+
// All bundled skills are always-on, so any inventory triggers the declaration.
281203
inv, err := NewInventory(translations.NullTranslationHelper).
282204
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
283205
Build()
@@ -291,23 +213,9 @@ func Test_DeclareSkillsExtensionIfEnabled(t *testing.T) {
291213
assert.True(t, ok, "always-on skills register, so the extension must be declared")
292214
})
293215

294-
t.Run("declares when notifications enabled (any skill triggers declaration)", func(t *testing.T) {
295-
inv, err := NewInventory(translations.NullTranslationHelper).
296-
WithToolsets([]string{string(ToolsetMetadataNotifications.ID)}).
297-
Build()
298-
require.NoError(t, err)
299-
300-
opts := &mcp.ServerOptions{}
301-
DeclareSkillsExtensionIfEnabled(opts, inv)
302-
303-
require.NotNil(t, opts.Capabilities)
304-
_, ok := opts.Capabilities.Extensions[skills.ExtensionKey]
305-
assert.True(t, ok, "skills extension must be declared when any bundled skill is enabled")
306-
})
307-
308216
t.Run("preserves other extensions already declared", func(t *testing.T) {
309217
inv, err := NewInventory(translations.NullTranslationHelper).
310-
WithToolsets([]string{string(ToolsetMetadataPullRequests.ID)}).
218+
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
311219
Build()
312220
require.NoError(t, err)
313221

@@ -323,6 +231,17 @@ func Test_DeclareSkillsExtensionIfEnabled(t *testing.T) {
323231
assert.True(t, hasSkills)
324232
assert.True(t, hasOther, "existing extensions must not be overwritten")
325233
})
234+
235+
t.Run("does not declare for an empty registry", func(t *testing.T) {
236+
// Tests the Registry's failure path directly — bypasses bundledSkills()
237+
// since no shipped configuration produces an empty registry.
238+
opts := &mcp.ServerOptions{}
239+
skills.New().DeclareCapability(opts)
240+
if opts.Capabilities != nil {
241+
_, ok := opts.Capabilities.Extensions[skills.ExtensionKey]
242+
assert.False(t, ok, "empty registry must not declare the extension")
243+
}
244+
})
326245
}
327246

328247
// Test_BundledSkills_AllRegistered_WhenAllToolsetsEnabled verifies that with the

skills/bundled.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ package skills
1212

1313
import _ "embed"
1414

15-
//go:embed pull-requests/SKILL.md
16-
var PullRequestsSKILL string
15+
//go:embed review-pr/SKILL.md
16+
var ReviewPRSKILL string
1717

18-
//go:embed inbox-triage/SKILL.md
19-
var InboxTriageSKILL string
18+
//go:embed handle-notifications/SKILL.md
19+
var HandleNotificationsSKILL string
2020

2121
//go:embed get-context/SKILL.md
2222
var GetContextSKILL string

0 commit comments

Comments
 (0)