Skip to content
Open
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.
14 changes: 14 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,20 @@ 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.

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.
14 changes: 14 additions & 0 deletions internal/docs/server.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,20 @@ 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.

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
183 changes: 183 additions & 0 deletions skills/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by the license
// that can be found in the LICENSE file.

package skills

import (
"context"
"fmt"
"iter"
"maps"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

// AddClient registers the Skills extension methods that client may send.
func AddClient(client *mcp.Client) error {
if client == nil {
return fmt.Errorf("skills: nil client")
}
if err := mcp.AddSendingCustomMethod[*ListSkillsParams, *ListSkillsResult](client, MethodList); err != nil {
return err
}
if err := mcp.AddSendingCustomMethod[*GetSkillParams, *GetSkillResult](client, MethodGet); err != nil {
return err
}
return mcp.AddSendingCustomMethod[*ReadDirectoryParams, *ReadDirectoryResult](client, MethodReadDirectory)
}

// List calls skills/list and validates the response.
func List(ctx context.Context, session *mcp.ClientSession, params *ListSkillsParams) (*ListSkillsResult, error) {
if err := requireCapability(session, false); err != nil {
return nil, err
}
if params == nil {
params = &ListSkillsParams{}
}
result, err := mcp.CallCustomMethod[*ListSkillsParams, *ListSkillsResult](ctx, session, MethodList, params)
if err != nil {
return nil, err
}
if err := validateListResponse(ctx, result); err != nil {
return nil, fmt.Errorf("skills: server returned an invalid skills/list result: %w", err)
}
return result, nil
}

// Get calls skills/get and validates the response.
func Get(ctx context.Context, session *mcp.ClientSession, params *GetSkillParams) (*GetSkillResult, error) {
if err := requireCapability(session, false); err != nil {
return nil, err
}
if params == nil || params.URI == "" {
return nil, fmt.Errorf("skills: get requires a URI")
}
result, err := mcp.CallCustomMethod[*GetSkillParams, *GetSkillResult](ctx, session, MethodGet, params)
if err != nil {
return nil, err
}
if result == nil || result.Skill == nil {
return nil, fmt.Errorf("skills: server returned a nil skill")
}
if result.Skill.URI != params.URI {
return nil, fmt.Errorf("skills: server returned URI %q for %q", result.Skill.URI, params.URI)
}
if err := ValidateSkill(result.Skill); err != nil {
return nil, fmt.Errorf("skills: server returned an invalid skill: %w", err)
}
return result, nil
}

// ReadDirectory calls resources/directory/read and validates the response.
func ReadDirectory(ctx context.Context, session *mcp.ClientSession, params *ReadDirectoryParams) (*ReadDirectoryResult, error) {
if err := requireCapability(session, true); err != nil {
return nil, err
}
if params == nil || params.URI == "" {
return nil, fmt.Errorf("skills: directory read requires a URI")
}
result, err := mcp.CallCustomMethod[*ReadDirectoryParams, *ReadDirectoryResult](ctx, session, MethodReadDirectory, params)
if err != nil {
return nil, err
}
if err := ValidateDirectoryResult(params.URI, result); err != nil {
return nil, fmt.Errorf("skills: server returned an invalid directory result: %w", err)
}
return result, nil
}

// All returns an iterator that follows every page of skills/list.
func All(ctx context.Context, session *mcp.ClientSession, params *ListSkillsParams) iter.Seq2[*Skill, error] {
var initial ListSkillsParams
if params != nil {
initial = *params
initial.Meta = maps.Clone(params.Meta)
}
return func(yield func(*Skill, error) bool) {
request := initial
allPages(initial.Cursor, func(cursor string) ([]*Skill, string, error) {
request.Cursor = cursor
result, err := List(ctx, session, &request)
if err != nil {
return nil, "", err
}
return result.Skills, result.NextCursor, nil
})(yield)
}
}

// DirectoryEntries returns an iterator that follows every page of a directory read.
func DirectoryEntries(ctx context.Context, session *mcp.ClientSession, params *ReadDirectoryParams) iter.Seq2[*mcp.Resource, error] {
var initial ReadDirectoryParams
if params != nil {
initial = *params
initial.Meta = maps.Clone(params.Meta)
}
return func(yield func(*mcp.Resource, error) bool) {
request := initial
allPages(initial.Cursor, func(cursor string) ([]*mcp.Resource, string, error) {
request.Cursor = cursor
result, err := ReadDirectory(ctx, session, &request)
if err != nil {
return nil, "", err
}
return result.Resources, result.NextCursor, nil
})(yield)
}
}

func allPages[T any](initialCursor string, fetch func(string) ([]T, string, error)) iter.Seq2[T, error] {
return func(yield func(T, error) bool) {
cursor := initialCursor
seen := map[string]bool{}
if cursor != "" {
seen[cursor] = true
}
for {
items, next, err := fetch(cursor)
if err != nil {
var zero T
yield(zero, err)
return
}
for _, item := range items {
if !yield(item, nil) {
return
}
}
if next == "" {
return
}
if seen[next] {
var zero T
yield(zero, fmt.Errorf("skills: server repeated pagination cursor %q", next))
return
}
seen[next] = true
cursor = next
}
}
}

func requireCapability(session *mcp.ClientSession, directoryRead bool) error {
if session == nil || session.InitializeResult() == nil || session.InitializeResult().Capabilities == nil {
return fmt.Errorf("skills: session has no server capabilities")
}
settings, ok := session.InitializeResult().Capabilities.Extensions[ExtensionID]
if !ok {
return fmt.Errorf("skills: server does not advertise %s", ExtensionID)
}
if !directoryRead {
return nil
}
m, ok := settings.(map[string]any)
if !ok {
return fmt.Errorf("skills: server advertised invalid extension settings")
}
enabled, _ := m["directoryRead"].(bool)
if !enabled {
return fmt.Errorf("skills: server does not advertise directoryRead")
}
return nil
}
39 changes: 39 additions & 0 deletions skills/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by the license
// that can be found in the LICENSE file.

package skills_test

import (
"context"
"log"

"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/modelcontextprotocol/go-sdk/skills"
)

func ExampleAddHandlers() {
server := mcp.NewServer(&mcp.Implementation{Name: "skills", Version: "v1.0.0"}, nil)
entry := &skills.Skill{
URI: "skill://generated/SKILL.md",
Frontmatter: skills.Frontmatter{
"name": "generated", "description": "Instructions generated on demand.",
},
Resources: skills.DynamicResources(),
}
err := skills.AddHandlers(server, &skills.Handlers{
List: func(context.Context, *mcp.ServerSession, *skills.ListSkillsParams) (*skills.ListSkillsResult, error) {
return &skills.ListSkillsResult{Skills: []*skills.Skill{entry}}, nil
},
Get: func(_ context.Context, _ *mcp.ServerSession, params *skills.GetSkillParams) (*skills.GetSkillResult, error) {
if params.URI != entry.URI {
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: "unknown skill"}
}
return &skills.GetSkillResult{Skill: entry}, nil
},
}, nil)
if err != nil {
log.Fatal(err)
}
}
Loading
Loading