Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,3 +546,8 @@ that optional capabilities outside the core protocol can be declared on the
wire. Keys are namespaced as `"{vendor-prefix}/{extension-name}"`; values
are per-extension settings objects.

The [`skills`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/skills)
package provides typed clients for SEP-2640. Call `skills.AddClient` before
connecting, then use `skills.List`, `skills.Get`, or `skills.ReadDirectory`.
The `skills.All` and `skills.DirectoryEntries` iterators follow pagination
cursors automatically without modifying caller-owned parameters.
96 changes: 96 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,102 @@ capabilities outside the core protocol can be declared on the wire. Keys
are namespaced as `"{vendor-prefix}/{extension-name}"`; values are
per-extension settings objects.

#### Skills extension

The [`skills`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/skills)
package implements SEP-2640. Use `skills.AddHandlers` to provide custom
`skills/list` and `skills/get` handlers. An optional directory handler enables
`resources/directory/read` and advertises `directoryRead: true`.

Custom providers may return `skills.DynamicResources()` for generated skills
that cannot publish stable file digests.

For skills stored on disk, `skills.AddDirectory` installs a filesystem-backed
provider. By default it discovers skills and files on every request, so
resources added after server startup are available without re-registering
them. This applies to `skills/list`, `skills/get`, `resources/list`,
`resources/read`, and `resources/directory/read`. Changes and removals are
visible on the next request too.

```go
if err := skills.AddDirectory(server, "./skills", &skills.DirectoryOptions{
PageSize: 100,
}); err != nil {
return err
}
```

`resources/list` includes the skill files alongside ordinary registered
resources. `SKILL.md` entries carry their frontmatter name and description and
the `text/markdown` MIME type. Directory reads return only direct children,
including subdirectories with MIME type `inode/directory`, and use the same
file metadata. Neither listing reads supporting file contents just to enumerate
them; `skills/list` and `skills/get` also hash files to build static manifests.

The helper combines all pages of the underlying resource listing with its
current catalog, deduplicates by URI, and paginates the result using
`DirectoryOptions.PageSize`. Exact resource registrations take precedence over
the template, for both listing and reading. This uses existing receiving
middleware and resource-template routing; it does not change the core SDK APIs.
The merge costs a traversal of the underlying resource list on each request.

Live discovery does not require `skills.DynamicResources()`: that marker means
the server cannot provide a complete manifest with stable digests, not that its
catalog changes over time. A filesystem provider returns a complete static
manifest for the current catalog; a later request can return a different one.

Clients must re-list to see changes. The helper starts no watcher and sends no
filesystem-change notifications; SEP-2640 defines no `skills/list_changed`
notification. The merged `resources/list` response has a zero TTL and private
cache scope so a TTL configured for ordinary resources cannot conceal changes.
These cache fields are omitted on older protocol versions by the core SDK.

Set `Cache` to `&skills.DirectoryCacheOptions{}` to load the catalog on the
first request and cache it indefinitely. Set `Preload: true` to load it while
constructing the provider, which also makes the constructor report initial scan
and validation errors. A positive `MaxAge` expires the cache after that
duration; the first request after expiry rebuilds it.

Cached providers can also be invalidated by a clock or filesystem monitor through
`DirectoryCacheOptions.Invalidate`. Signals are coalesced and consumed when a
request arrives; use a buffered channel so producers do not block. Both
`MaxAge` and `Invalidate` are lazy: they do not start a background goroutine.
A failed rebuild retains the previous catalog but leaves it stale: requests
retry rebuilding until successful, even if the invalidation signal has already
been consumed. They return the scan error instead of silently serving stale
metadata. Resource bytes are always read on demand, including in cached modes.

To rebuild before the next request, construct a provider directly and call
`Refresh` from the application's watcher goroutine. The caller owns goroutine
lifetime, cancellation, and error handling:

```go
provider, err := skills.NewDirectoryProvider("./skills", &skills.DirectoryOptions{
Cache: &skills.DirectoryCacheOptions{Preload: true},
})
if err != nil {
return err
}
if err := provider.AddTo(server); err != nil {
return err
}
go func() {
for range changed {
if err := provider.Refresh(ctx); err != nil {
logger.Error("refreshing skills", "error", err)
}
}
}()
```

Register one filesystem provider per server. Applications that need to combine
multiple filesystems can use an overlay `fs.FS` or aggregate them behind custom
`AddHandlers` handlers.

SEP validation is enabled by default, including the 512-resource and 16 MiB
per-skill limits. `skills.ServerOptions` supports additional validators and
explicit unsafe overrides.

### Pagination

Server-side feature lists may be
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
golang.org/x/oauth2 v0.35.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.42.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5 changes: 5 additions & 0 deletions internal/docs/client.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,8 @@ that optional capabilities outside the core protocol can be declared on the
wire. Keys are namespaced as `"{vendor-prefix}/{extension-name}"`; values
are per-extension settings objects.

The [`skills`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/skills)
package provides typed clients for SEP-2640. Call `skills.AddClient` before
connecting, then use `skills.List`, `skills.Get`, or `skills.ReadDirectory`.
The `skills.All` and `skills.DirectoryEntries` iterators follow pagination
cursors automatically without modifying caller-owned parameters.
96 changes: 96 additions & 0 deletions internal/docs/server.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,102 @@ capabilities outside the core protocol can be declared on the wire. Keys
are namespaced as `"{vendor-prefix}/{extension-name}"`; values are
per-extension settings objects.

#### Skills extension

The [`skills`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/skills)
package implements SEP-2640. Use `skills.AddHandlers` to provide custom
`skills/list` and `skills/get` handlers. An optional directory handler enables
`resources/directory/read` and advertises `directoryRead: true`.

Custom providers may return `skills.DynamicResources()` for generated skills
that cannot publish stable file digests.

For skills stored on disk, `skills.AddDirectory` installs a filesystem-backed
provider. By default it discovers skills and files on every request, so
resources added after server startup are available without re-registering
them. This applies to `skills/list`, `skills/get`, `resources/list`,
`resources/read`, and `resources/directory/read`. Changes and removals are
visible on the next request too.

```go
if err := skills.AddDirectory(server, "./skills", &skills.DirectoryOptions{
PageSize: 100,
}); err != nil {
return err
}
```

`resources/list` includes the skill files alongside ordinary registered
resources. `SKILL.md` entries carry their frontmatter name and description and
the `text/markdown` MIME type. Directory reads return only direct children,
including subdirectories with MIME type `inode/directory`, and use the same
file metadata. Neither listing reads supporting file contents just to enumerate
them; `skills/list` and `skills/get` also hash files to build static manifests.

The helper combines all pages of the underlying resource listing with its
current catalog, deduplicates by URI, and paginates the result using
`DirectoryOptions.PageSize`. Exact resource registrations take precedence over
the template, for both listing and reading. This uses existing receiving
middleware and resource-template routing; it does not change the core SDK APIs.
The merge costs a traversal of the underlying resource list on each request.

Live discovery does not require `skills.DynamicResources()`: that marker means
the server cannot provide a complete manifest with stable digests, not that its
catalog changes over time. A filesystem provider returns a complete static
manifest for the current catalog; a later request can return a different one.

Clients must re-list to see changes. The helper starts no watcher and sends no
filesystem-change notifications; SEP-2640 defines no `skills/list_changed`
notification. The merged `resources/list` response has a zero TTL and private
cache scope so a TTL configured for ordinary resources cannot conceal changes.
These cache fields are omitted on older protocol versions by the core SDK.

Set `Cache` to `&skills.DirectoryCacheOptions{}` to load the catalog on the
first request and cache it indefinitely. Set `Preload: true` to load it while
constructing the provider, which also makes the constructor report initial scan
and validation errors. A positive `MaxAge` expires the cache after that
duration; the first request after expiry rebuilds it.

Cached providers can also be invalidated by a clock or filesystem monitor through
`DirectoryCacheOptions.Invalidate`. Signals are coalesced and consumed when a
request arrives; use a buffered channel so producers do not block. Both
`MaxAge` and `Invalidate` are lazy: they do not start a background goroutine.
A failed rebuild retains the previous catalog but leaves it stale: requests
retry rebuilding until successful, even if the invalidation signal has already
been consumed. They return the scan error instead of silently serving stale
metadata. Resource bytes are always read on demand, including in cached modes.

To rebuild before the next request, construct a provider directly and call
`Refresh` from the application's watcher goroutine. The caller owns goroutine
lifetime, cancellation, and error handling:

```go
provider, err := skills.NewDirectoryProvider("./skills", &skills.DirectoryOptions{
Cache: &skills.DirectoryCacheOptions{Preload: true},
})
if err != nil {
return err
}
if err := provider.AddTo(server); err != nil {
return err
}
go func() {
for range changed {
if err := provider.Refresh(ctx); err != nil {
logger.Error("refreshing skills", "error", err)
}
}
}()
```

Register one filesystem provider per server. Applications that need to combine
multiple filesystems can use an overlay `fs.FS` or aggregate them behind custom
`AddHandlers` handlers.

SEP validation is enabled by default, including the 512-resource and 16 MiB
per-skill limits. `skills.ServerOptions` supports additional validators and
explicit unsafe overrides.

### Pagination

Server-side feature lists may be
Expand Down
16 changes: 16 additions & 0 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,22 @@ type ServerOptions struct {
SupportedProtocolVersions []string
}

// AddExtension adds an extension capability to the server.
//
// Extensions should normally be added before the server accepts connections,
// so that clients observe them during capability negotiation. If settings is
// nil, an empty object is advertised.
func (s *Server) AddExtension(name string, settings map[string]any) {
s.mu.Lock()
defer s.mu.Unlock()
if s.opts.Capabilities == nil {
s.opts.Capabilities = &ServerCapabilities{Logging: &LoggingCapabilities{}}
} else {
s.opts.Capabilities = s.opts.Capabilities.clone()
}
s.opts.Capabilities.AddExtension(name, maps.Clone(settings))
}

// NewServer creates a new MCP server. The resulting server has no features:
// add features using the various Server.AddXXX methods, and the [AddTool] function.
//
Expand Down
25 changes: 25 additions & 0 deletions mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,31 @@ func TestServerCapabilities(t *testing.T) {
}
}

func TestServerAddExtension(t *testing.T) {
capabilities := &ServerCapabilities{Tools: &ToolCapabilities{}}
server := NewServer(testImpl, &ServerOptions{Capabilities: capabilities})
settings := map[string]any{"enabled": true}
server.AddExtension("io.example/test", settings)
settings["enabled"] = false

got := server.capabilities().Extensions["io.example/test"]
want := map[string]any{"enabled": true}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("extension settings mismatch (-want +got):\n%s", diff)
}
if capabilities.Extensions != nil {
t.Fatal("AddExtension mutated the caller's capabilities")
}
}

func TestServerAddExtensionPreservesDefaultCapabilities(t *testing.T) {
server := NewServer(testImpl, nil)
server.AddExtension("io.example/test", nil)
if server.capabilities().Logging == nil {
t.Fatal("AddExtension removed the default logging capability")
}
}

func TestServerAddResourceTemplate(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading