Skip to content

Commit 66b21fa

Browse files
olaservoclaude
andcommitted
feat(skills): add per-repo MCP resource template + skills toolset
Adds the SEP-2640-aligned per-repo Agent Skills surface: a single parameterized MCP resource template that lets any GitHub repo expose its skills/ directory through the same skill:// URI scheme used for bundled skills. Template: skill://{owner}/{repo}/{skill_name}/{+file_path} `{+file_path}` is RFC 6570 reserved expansion, so a multi-segment relative path inside the skill directory (e.g. references/GUIDE.md) round-trips through the template as a single value. SKILL.md and any relative references mentioned inside it both resolve via this one template handler. Discovery on the server: - Recognizes the four agentskills.io directory conventions: skills/<name>/SKILL.md, skills/<namespace>/<name>/SKILL.md, plugins/<plugin>/skills/<name>/SKILL.md, and root-level <name>/SKILL.md (excluding hidden + convention-prefix dirs). - Skill discovery happens at request time via the Git Trees API. - skill:// completion is wired through CompletionsHandler so MCP hosts can offer interactive autocomplete on owner/repo/skill_name. Index integration: - skills.Registry extended with BundledTemplate + AddTemplate + EnabledTemplates so skill://index.json now publishes both type:"skill-md" entries (the 27 bundled skills) and a single type:"mcp-resource-template" entry (the per-repo template). - IndexEntry.Name is now omitempty since the SEP example shows template entries without a name. - DeclareCapability fires for either type of entry being enabled. Toolset gating: - New non-default `skills` toolset gates the per-repo template's index entry. With --toolsets=default the per-repo surface is hidden; with --toolsets=default,skills (or --toolsets=all) it's published. - The per-repo template is defined in pkg/github/skills_resource.go; registered via AllResources so the inventory wires it up alongside repo:// templates. Adapted from the resource-template work in #2129. The non-SEP _manifest endpoint from that PR is intentionally NOT ported — multi-file skill discovery is handled by the SEP's per-file URI resolution, not a manifest JSON. Tests cover: template definition, file handler with SKILL.md, file handler with multi-segment relative path, missing-arg errors, path traversal rejection, agentskills.io convention discovery, URI parsing, completion handler, and the index-entry presence/absence based on toolset gating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9c92f7d commit 66b21fa

8 files changed

Lines changed: 994 additions & 17 deletions

File tree

pkg/github/bundled_skills.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ import (
1313
// The Registry's `Enabled` closure is still available for future use
1414
// (e.g. feature-flagging a skill behind an experimental toolset).
1515
//
16+
// In addition to the bundled SKILL.md catalogue, the registry advertises
17+
// the per-repo skill resource template (skill://{owner}/{repo}/{skill_name}/{+file_path})
18+
// in the discovery index when the `skills` toolset is enabled. The matching
19+
// MCP resource template + handler are registered via the inventory.
20+
//
1621
// Adding a new server-bundled skill is one entry here plus a //go:embed
1722
// line in package skills.
18-
func bundledSkills(_ *inventory.Inventory) *skills.Registry {
23+
func bundledSkills(inv *inventory.Inventory) *skills.Registry {
1924
return skills.New().
2025
Add(skills.Bundled{
2126
Name: "review-pr",
@@ -151,6 +156,14 @@ func bundledSkills(_ *inventory.Inventory) *skills.Registry {
151156
Name: "share-snippet",
152157
Description: "Create and manage code snippets via GitHub Gists. Use when sharing a code snippet, creating a quick paste, saving notes as a gist, or managing your existing gists.",
153158
Content: skills.ShareSnippetSKILL,
159+
}).
160+
// Per-repo skill template (SEP-2640 mcp-resource-template entry).
161+
// Gated on the `skills` toolset since the matching MCP resource
162+
// template is registered there too.
163+
AddTemplate(skills.BundledTemplate{
164+
Description: "Agent Skills hosted in any GitHub repository — fill in {owner}/{repo}/{skill_name} to read SKILL.md, then extend the URI to read referenced files (e.g. references/GUIDE.md).",
165+
URL: SkillResourceDiscoveryURL,
166+
Enabled: func() bool { return inv.IsToolsetEnabled(ToolsetMetadataSkills.ID) },
154167
})
155168
}
156169

pkg/github/bundled_skills_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,69 @@ func Test_DeclareSkillsExtensionIfEnabled(t *testing.T) {
242242
})
243243
}
244244

245+
// Test_BundledSkills_TemplateInIndex_WhenSkillsToolsetEnabled verifies that
246+
// enabling the `skills` toolset causes the per-repo skill template entry to
247+
// appear in `skill://index.json` with `type: "mcp-resource-template"`. This
248+
// is the SEP-2640 discovery story for parameterized skill families.
249+
func Test_BundledSkills_TemplateInIndex_WhenSkillsToolsetEnabled(t *testing.T) {
250+
ctx := context.Background()
251+
inv, err := NewInventory(translations.NullTranslationHelper).
252+
WithToolsets([]string{string(ToolsetMetadataSkills.ID)}).
253+
Build()
254+
require.NoError(t, err)
255+
256+
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
257+
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
258+
})
259+
RegisterBundledSkills(srv, inv)
260+
261+
session := connectClient(t, ctx, srv)
262+
res, err := session.ReadResource(ctx, &mcp.ReadResourceParams{URI: skills.IndexURI})
263+
require.NoError(t, err)
264+
265+
var idx skills.IndexDoc
266+
require.NoError(t, json.Unmarshal([]byte(res.Contents[0].Text), &idx))
267+
268+
var found *skills.IndexEntry
269+
for i := range idx.Skills {
270+
if idx.Skills[i].Type == "mcp-resource-template" {
271+
found = &idx.Skills[i]
272+
break
273+
}
274+
}
275+
require.NotNil(t, found, "index must include an mcp-resource-template entry when skills toolset is enabled")
276+
assert.Equal(t, SkillResourceDiscoveryURL, found.URL)
277+
assert.Empty(t, found.Name, "mcp-resource-template entries omit `name` per SEP example")
278+
assert.NotEmpty(t, found.Description)
279+
}
280+
281+
// Test_BundledSkills_TemplateAbsent_WhenSkillsToolsetDisabled verifies that
282+
// without the `skills` toolset, the template is not advertised — but the
283+
// always-on bundled skills still are.
284+
func Test_BundledSkills_TemplateAbsent_WhenSkillsToolsetDisabled(t *testing.T) {
285+
ctx := context.Background()
286+
inv, err := NewInventory(translations.NullTranslationHelper).
287+
WithToolsets([]string{string(ToolsetMetadataContext.ID)}).
288+
Build()
289+
require.NoError(t, err)
290+
291+
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, &mcp.ServerOptions{
292+
Capabilities: &mcp.ServerCapabilities{Resources: &mcp.ResourceCapabilities{}},
293+
})
294+
RegisterBundledSkills(srv, inv)
295+
296+
session := connectClient(t, ctx, srv)
297+
res, err := session.ReadResource(ctx, &mcp.ReadResourceParams{URI: skills.IndexURI})
298+
require.NoError(t, err)
299+
300+
var idx skills.IndexDoc
301+
require.NoError(t, json.Unmarshal([]byte(res.Contents[0].Text), &idx))
302+
303+
for _, entry := range idx.Skills {
304+
assert.NotEqual(t, "mcp-resource-template", entry.Type, "template entry must not appear when skills toolset disabled")
305+
}
306+
}
307+
245308
// Test_BundledSkills_NoDuplicateURIs guards against accidental duplicate
246309
// registrations — two skills with the same name would collide on the same
247310
// skill://github/<name>/SKILL.md URI.

pkg/github/resources.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,9 @@ func AllResources(t translations.TranslationHelperFunc) []inventory.ServerResour
1515
GetRepositoryResourceCommitContent(t),
1616
GetRepositoryResourceTagContent(t),
1717
GetRepositoryResourcePrContent(t),
18+
19+
// Skill resources (SEP-2640): per-file template for any skill in any GitHub repo.
20+
// Gated on the `skills` toolset.
21+
GetSkillResourceFile(t),
1822
}
1923
}

pkg/github/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ func CompletionsHandler(getClient GetClientFn) func(ctx context.Context, req *mc
219219
if strings.HasPrefix(req.Params.Ref.URI, "repo://") {
220220
return RepositoryResourceCompletionHandler(getClient)(ctx, req)
221221
}
222+
if strings.HasPrefix(req.Params.Ref.URI, "skill://") {
223+
return SkillResourceCompletionHandler(getClient)(ctx, req)
224+
}
222225
return nil, fmt.Errorf("unsupported resource URI: %s", req.Params.Ref.URI)
223226
case "ref/prompt":
224227
return nil, nil

0 commit comments

Comments
 (0)