Skip to content

Commit f45a34c

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 switches them from toolset-gated loading to always-on, matching how upstream's skills load. - pull-requests → review-pr - inbox-triage → handle-notifications Both renames preserve the original content (parameter-level workflow detail for review-pr; High/Medium/Low priority taxonomy + read/done semantics for handle-notifications). Only the names change, plus the cross-reference inside handle-notifications/SKILL.md (was → review-pr). Load consistency: - Drop the per-skill `Enabled func() bool` closures so both skills register unconditionally, matching upstream's "everything always available" semantics. - Drop the per-skill `Icons` field. The `light-bulb` octicon used by pull-requests didn't resolve in this fork's icon set anyway, and shipping no icons is the simpler, more consistent default. - The Registry's `Enabled` closure mechanism stays in place for future use; just no shipped skill currently exercises it. A new Test_DeclareSkillsExtensionIfEnabled subtest covers the empty-registry 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 from 4 subtests to 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d202ea6 commit f45a34c

5 files changed

Lines changed: 97 additions & 165 deletions

File tree

pkg/github/bundled_skills.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +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.
11-
// Each entry's Enabled closure gates its publication on the relevant
12-
// toolset being enabled under the given inventory.
10+
//
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).
1315
//
1416
// Adding a new server-bundled skill is one entry here plus a //go:embed
1517
// line in package skills.
16-
func bundledSkills(inv *inventory.Inventory) *skills.Registry {
18+
func bundledSkills(_ *inventory.Inventory) *skills.Registry {
1719
return skills.New().
1820
Add(skills.Bundled{
19-
Name: "pull-requests",
20-
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.",
21-
Content: skills.PullRequestsSKILL,
22-
Icons: octicons.Icons("light-bulb"),
23-
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,
2424
}).
2525
Add(skills.Bundled{
26-
Name: "inbox-triage",
26+
Name: "handle-notifications",
2727
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.",
28-
Content: skills.InboxTriageSKILL,
29-
Icons: octicons.Icons("bell"),
30-
Enabled: func() bool { return inv.IsToolsetEnabled(ToolsetMetadataNotifications.ID) },
28+
Content: skills.HandleNotificationsSKILL,
3129
})
3230
}
3331

pkg/github/bundled_skills_test.go

Lines changed: 79 additions & 145 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 bundled skills, derived from skills.Bundled so tests never drift from
18+
// 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,98 +74,40 @@ 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 but no shipped skill currently uses it.
89+
func Test_BundledSkills_RegisterRegardlessOfToolset(t *testing.T) {
8890
ctx := context.Background()
8991

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 when pull_requests toolset 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-
for _, r := range listResources(t, ctx, srv) {
121-
assert.NotEqual(t, pullRequestsSkillURI, r.URI)
122-
assert.NotEqual(t, inboxTriageSkillURI, r.URI)
123-
assert.NotEqual(t, skills.IndexURI, r.URI)
124-
}
125-
})
126-
127-
t.Run("registers inbox-triage when notifications toolset enabled", func(t *testing.T) {
128-
inv, err := NewInventory(translations.NullTranslationHelper).
129-
WithToolsets([]string{string(ToolsetMetadataNotifications.ID)}).
130-
Build()
131-
require.NoError(t, err)
132-
133-
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
134-
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
135-
})
136-
RegisterBundledSkills(srv, inv)
92+
// Pick a minimal toolset (context only) — doesn't enable pull_requests
93+
// or notifications, the toolsets the renamed skills used to be gated on.
94+
inv, err := NewInventory(translations.NullTranslationHelper).
95+
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
96+
Build()
97+
require.NoError(t, err)
13798

138-
uris := map[string]string{}
139-
for _, r := range listResources(t, ctx, srv) {
140-
uris[r.URI] = r.MIMEType
141-
}
142-
assert.Equal(t, "text/markdown", uris[inboxTriageSkillURI])
143-
assert.NotContains(t, uris, pullRequestsSkillURI, "only notifications enabled — pull-requests should not be registered")
144-
assert.Equal(t, "application/json", uris[skills.IndexURI])
99+
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
100+
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
145101
})
102+
RegisterBundledSkills(srv, inv)
146103

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

171113
// Test_BundledSkills_ReadContent verifies that reading the skill resource
@@ -174,7 +116,7 @@ func Test_BundledSkills_Registration(t *testing.T) {
174116
func Test_BundledSkills_ReadContent(t *testing.T) {
175117
ctx := context.Background()
176118
inv, err := NewInventory(translations.NullTranslationHelper).
177-
WithToolsets([]string{string(ToolsetMetadataPullRequests.ID)}).
119+
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
178120
Build()
179121
require.NoError(t, err)
180122

@@ -186,11 +128,11 @@ func Test_BundledSkills_ReadContent(t *testing.T) {
186128
session := connectClient(t, ctx, srv)
187129

188130
t.Run("SKILL.md content", func(t *testing.T) {
189-
res, err := session.ReadResource(ctx, &mcp.ReadResourceParams{URI: pullRequestsSkillURI})
131+
res, err := session.ReadResource(ctx, &mcp.ReadResourceParams{URI: reviewPRSkillURI})
190132
require.NoError(t, err)
191133
require.Len(t, res.Contents, 1)
192134
assert.Equal(t, "text/markdown", res.Contents[0].MIMEType)
193-
assert.Equal(t, skills.PullRequestsSKILL, res.Contents[0].Text)
135+
assert.Equal(t, skills.ReviewPRSKILL, res.Contents[0].Text)
194136
})
195137

196138
t.Run("index.json matches SEP discovery schema", func(t *testing.T) {
@@ -202,23 +144,31 @@ func Test_BundledSkills_ReadContent(t *testing.T) {
202144
var idx skills.IndexDoc
203145
require.NoError(t, json.Unmarshal([]byte(res.Contents[0].Text), &idx))
204146
assert.Equal(t, skills.IndexSchema, idx.Schema)
205-
require.Len(t, idx.Skills, 1)
206-
assert.Equal(t, "pull-requests", idx.Skills[0].Name)
207-
assert.Equal(t, "skill-md", idx.Skills[0].Type)
208-
assert.Equal(t, pullRequestsSkillURI, idx.Skills[0].URL)
209-
assert.NotEmpty(t, idx.Skills[0].Description)
147+
148+
// Index size must equal the number of currently-enabled bundled skills.
149+
assert.Len(t, idx.Skills, len(bundledSkills(inv).Enabled()))
150+
151+
// The review-pr skill must be present.
152+
var found *skills.IndexEntry
153+
for i := range idx.Skills {
154+
if idx.Skills[i].Name == "review-pr" {
155+
found = &idx.Skills[i]
156+
break
157+
}
158+
}
159+
require.NotNil(t, found, "review-pr must appear in the index")
160+
assert.Equal(t, "skill-md", found.Type)
161+
assert.Equal(t, reviewPRSkillURI, found.URL)
162+
assert.NotEmpty(t, found.Description)
210163
})
211164
}
212165

213-
// Test_BundledSkills_Index_MultipleSkills verifies that all enabled skills
166+
// Test_BundledSkills_Index_MultipleSkills verifies that all bundled skills
214167
// appear in the discovery index, not just the first one.
215168
func Test_BundledSkills_Index_MultipleSkills(t *testing.T) {
216169
ctx := context.Background()
217170
inv, err := NewInventory(translations.NullTranslationHelper).
218-
WithToolsets([]string{
219-
string(ToolsetMetadataPullRequests.ID),
220-
string(ToolsetMetadataNotifications.ID),
221-
}).
171+
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
222172
Build()
223173
require.NoError(t, err)
224174

@@ -237,29 +187,17 @@ func Test_BundledSkills_Index_MultipleSkills(t *testing.T) {
237187
for _, s := range idx.Skills {
238188
names[s.Name] = s.URL
239189
}
240-
assert.Equal(t, pullRequestsSkillURI, names["pull-requests"])
241-
assert.Equal(t, inboxTriageSkillURI, names["inbox-triage"])
190+
assert.Equal(t, reviewPRSkillURI, names["review-pr"])
191+
assert.Equal(t, handleNotificationsSkillURI, names["handle-notifications"])
242192
}
243193

244194
// Test_DeclareSkillsExtensionIfEnabled verifies that the skills-over-MCP
245-
// extension (SEP-2133) is declared in ServerOptions.Capabilities when the
246-
// pull_requests toolset is enabled, and is absent when it is not.
195+
// extension (SEP-2133) is declared in ServerOptions.Capabilities whenever the
196+
// server has any bundled skill or template entry to publish, and that
197+
// declaration is additive (doesn't clobber other extensions).
247198
func Test_DeclareSkillsExtensionIfEnabled(t *testing.T) {
248-
t.Run("declares when pull_requests enabled", func(t *testing.T) {
249-
inv, err := NewInventory(translations.NullTranslationHelper).
250-
WithToolsets([]string{string(ToolsetMetadataPullRequests.ID)}).
251-
Build()
252-
require.NoError(t, err)
253-
254-
opts := &mcp.ServerOptions{}
255-
DeclareSkillsExtensionIfEnabled(opts, inv)
256-
257-
require.NotNil(t, opts.Capabilities)
258-
_, ok := opts.Capabilities.Extensions[skills.ExtensionKey]
259-
assert.True(t, ok, "skills extension must be declared")
260-
})
261-
262-
t.Run("does not declare when pull_requests disabled", func(t *testing.T) {
199+
t.Run("declares for any non-empty registry", func(t *testing.T) {
200+
// All bundled skills are always-on, so any inventory triggers the declaration.
263201
inv, err := NewInventory(translations.NullTranslationHelper).
264202
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
265203
Build()
@@ -268,29 +206,14 @@ func Test_DeclareSkillsExtensionIfEnabled(t *testing.T) {
268206
opts := &mcp.ServerOptions{}
269207
DeclareSkillsExtensionIfEnabled(opts, inv)
270208

271-
if opts.Capabilities != nil {
272-
_, ok := opts.Capabilities.Extensions[skills.ExtensionKey]
273-
assert.False(t, ok, "skills extension must NOT be declared when no skills will be registered")
274-
}
275-
})
276-
277-
t.Run("declares when notifications enabled (any skill triggers declaration)", func(t *testing.T) {
278-
inv, err := NewInventory(translations.NullTranslationHelper).
279-
WithToolsets([]string{string(ToolsetMetadataNotifications.ID)}).
280-
Build()
281-
require.NoError(t, err)
282-
283-
opts := &mcp.ServerOptions{}
284-
DeclareSkillsExtensionIfEnabled(opts, inv)
285-
286209
require.NotNil(t, opts.Capabilities)
287210
_, ok := opts.Capabilities.Extensions[skills.ExtensionKey]
288-
assert.True(t, ok, "skills extension must be declared when any bundled skill is enabled")
211+
assert.True(t, ok, "always-on skills register, so the extension must be declared")
289212
})
290213

291214
t.Run("preserves other extensions already declared", func(t *testing.T) {
292215
inv, err := NewInventory(translations.NullTranslationHelper).
293-
WithToolsets([]string{string(ToolsetMetadataPullRequests.ID)}).
216+
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
294217
Build()
295218
require.NoError(t, err)
296219

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

311245
// listResources enumerates resources/list via an in-memory client session.

skills/bundled.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ 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

0 commit comments

Comments
 (0)