Skip to content

Commit 31b4e04

Browse files
olaservoclaude
andcommitted
feat(skills): add Registry for publishing bundled Agent Skills over MCP
Introduce a reusable `skills` package that lets an MCP server publish server-bundled Agent Skills (SKILL.md files shipped in the binary) per the skills-over-MCP SEP (SEP-2133): - skills.Bundled describes one skill (name, description, embedded content, optional icons, optional enabled predicate for runtime gating on toolsets/feature-flags/headers) - skills.Registry collects entries, declares the `io.modelcontextprotocol/skills` extension capability on the server, and installs each SKILL.md as an MCP resource plus a skill://index.json discovery document conforming to the agentskills.io/discovery/0.2.0 schema The package has no GitHub-specific state — any MCP server author can drop it in to publish bundled skills with a small amount of wiring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 569a48d commit 31b4e04

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed

skills/registry.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package skills
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"github.com/modelcontextprotocol/go-sdk/mcp"
8+
)
9+
10+
// Well-known identifiers from the skills-over-MCP SEP (SEP-2133) and the
11+
// Agent Skills discovery index schema (agentskills.io).
12+
const (
13+
// IndexURI is the well-known URI for the per-server discovery index.
14+
IndexURI = "skill://index.json"
15+
// ExtensionKey is the MCP capability extension identifier that a server
16+
// MUST declare when it publishes skill:// resources.
17+
ExtensionKey = "io.modelcontextprotocol/skills"
18+
// IndexSchema is the $schema value servers MUST emit in their index.
19+
IndexSchema = "https://schemas.agentskills.io/discovery/0.2.0/schema.json"
20+
)
21+
22+
// Bundled describes a single server-bundled Agent Skill — a SKILL.md the
23+
// server ships in its binary and serves at a stable skill:// URI.
24+
type Bundled struct {
25+
// Name is the skill name. Must match the SKILL.md frontmatter `name`
26+
// and the final segment of the skill-path in the URI.
27+
Name string
28+
// Description is the text shown to the agent in the discovery index.
29+
// Should describe both what the skill does and when to use it.
30+
Description string
31+
// Content is the SKILL.md body (typically a //go:embed string).
32+
Content string
33+
// Icons, if non-empty, are attached to the SKILL.md MCP resource so
34+
// hosts that render icons in their resource list can show one.
35+
Icons []mcp.Icon
36+
// Enabled, if set, is called to determine whether this skill should
37+
// be published on the current server instance. Leave nil for "always
38+
// publish". Useful for gating on a toolset, feature flag, or request
39+
// context in per-request server builds.
40+
Enabled func() bool
41+
}
42+
43+
// URI returns the skill's canonical SKILL.md URI: skill://<name>/SKILL.md.
44+
func (b Bundled) URI() string { return "skill://" + b.Name + "/SKILL.md" }
45+
46+
func (b Bundled) enabled() bool { return b.Enabled == nil || b.Enabled() }
47+
48+
// Registry is the set of bundled skills a server publishes. Build one at
49+
// server-construction time with New().Add(...).Add(...); then call
50+
// DeclareCapability before mcp.NewServer and Install after.
51+
type Registry struct {
52+
entries []Bundled
53+
}
54+
55+
// New returns an empty registry.
56+
func New() *Registry { return &Registry{} }
57+
58+
// Add appends a bundled skill and returns the registry for chaining.
59+
func (r *Registry) Add(b Bundled) *Registry {
60+
r.entries = append(r.entries, b)
61+
return r
62+
}
63+
64+
// Enabled returns the subset of entries currently enabled.
65+
func (r *Registry) Enabled() []Bundled {
66+
var out []Bundled
67+
for _, e := range r.entries {
68+
if e.enabled() {
69+
out = append(out, e)
70+
}
71+
}
72+
return out
73+
}
74+
75+
// DeclareCapability adds the skills-over-MCP extension to the provided
76+
// ServerOptions.Capabilities if any entry is currently enabled. Must be
77+
// called BEFORE mcp.NewServer since capabilities are captured at
78+
// construction.
79+
func (r *Registry) DeclareCapability(opts *mcp.ServerOptions) {
80+
if opts == nil || len(r.Enabled()) == 0 {
81+
return
82+
}
83+
if opts.Capabilities == nil {
84+
opts.Capabilities = &mcp.ServerCapabilities{}
85+
}
86+
opts.Capabilities.AddExtension(ExtensionKey, nil)
87+
}
88+
89+
// Install registers each enabled skill's SKILL.md as an MCP resource and
90+
// publishes the skill://index.json discovery document.
91+
func (r *Registry) Install(s *mcp.Server) {
92+
enabled := r.Enabled()
93+
if len(enabled) == 0 {
94+
return
95+
}
96+
97+
for _, e := range enabled {
98+
e := e
99+
s.AddResource(
100+
&mcp.Resource{
101+
URI: e.URI(),
102+
Name: e.Name + "_skill",
103+
Description: e.Description,
104+
MIMEType: "text/markdown",
105+
Icons: e.Icons,
106+
},
107+
func(_ context.Context, _ *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
108+
return &mcp.ReadResourceResult{
109+
Contents: []*mcp.ResourceContents{{
110+
URI: e.URI(),
111+
MIMEType: "text/markdown",
112+
Text: e.Content,
113+
}},
114+
}, nil
115+
},
116+
)
117+
}
118+
119+
indexJSON := buildIndex(enabled)
120+
s.AddResource(
121+
&mcp.Resource{
122+
URI: IndexURI,
123+
Name: "skills_index",
124+
Description: "Agent Skill discovery index for this server.",
125+
MIMEType: "application/json",
126+
},
127+
func(_ context.Context, _ *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
128+
return &mcp.ReadResourceResult{
129+
Contents: []*mcp.ResourceContents{{
130+
URI: IndexURI,
131+
MIMEType: "application/json",
132+
Text: indexJSON,
133+
}},
134+
}, nil
135+
},
136+
)
137+
}
138+
139+
// IndexEntry matches the agentskills.io discovery schema, with MCP-specific
140+
// fields: `url` holds the MCP resource URI; `digest` is omitted because
141+
// integrity is handled by the authenticated MCP connection.
142+
type IndexEntry struct {
143+
Name string `json:"name"`
144+
Type string `json:"type"`
145+
Description string `json:"description"`
146+
URL string `json:"url"`
147+
}
148+
149+
// IndexDoc is the top-level shape of skill://index.json.
150+
type IndexDoc struct {
151+
Schema string `json:"$schema"`
152+
Skills []IndexEntry `json:"skills"`
153+
}
154+
155+
func buildIndex(entries []Bundled) string {
156+
doc := IndexDoc{Schema: IndexSchema, Skills: make([]IndexEntry, len(entries))}
157+
for i, e := range entries {
158+
doc.Skills[i] = IndexEntry{Name: e.Name, Type: "skill-md", Description: e.Description, URL: e.URI()}
159+
}
160+
b, err := json.Marshal(doc)
161+
if err != nil {
162+
panic("skills: failed to marshal index: " + err.Error())
163+
}
164+
return string(b)
165+
}

0 commit comments

Comments
 (0)