mcp: add Extensions field to capabilities per SEP-2133 (#794)

This implements
[SEP-2133](https://modelcontextprotocol.io/community/seps/2133-extensions)
and resolves #777.

## Summary of changes

- Add `Extensions` field to `ServerCapabilities` and
`ClientCapabilities` structs per
[SEP-2133](https://modelcontextprotocol.io/community/seps/2133-extensions)
- Add `AddExtension` helper methods on both types that normalize nil
settings to empty maps.
- [The spec requires an object, not
null](https://modelcontextprotocol.io/community/seps/2133-extensions#negotiation:~:text=an%20empty%20object%20indicates%20no%20settings.)
- Include shallow-copy support via `maps.Clone` in the existing
`clone()` methods
  - also include shallow-copy support for `Experimental` field.
- Add comprehensive tests: unit tests for nil-safety, capability
preservation, and over-the-wire round-trip

## What's tested

- [x] `go test ./mcp/...` passes
- [x] Server conformance tests pass (30 scenarios, 40 assertions, 0
failures)
- [x] Client conformance tests match existing baseline (auth scenarios
are known failures)
This commit is contained in:
Yamamoto, Hirotaka 2026-02-18 00:10:07 +09:00 • committed by GitHub
commit 3dd262606e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 124 additions and 2 deletions

View file

@ -402,6 +402,26 @@ func TestClientCapabilities(t *testing.T) {
RootsV2: &RootCapabilities{ListChanged: true},
},
},
{
name: "extensions preserved",
configureClient: func(s *Client) {},
clientOpts: func() ClientOptions {
caps := &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: true},
}
caps.AddExtension("io.example/ext1", map[string]any{"key": "value"})
caps.AddExtension("io.example/ext2", nil)
return ClientOptions{Capabilities: caps}
}(),
wantCapabilities: &ClientCapabilities{
Extensions: map[string]any{
"io.example/ext1": map[string]any{"key": "value"},
"io.example/ext2": map[string]any{},
},
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
},
},
}
for _, tc := range testCases {
@ -469,6 +489,23 @@ func TestClientCapabilitiesOverWire(t *testing.T) {
},
wantCapabilities: &ClientCapabilities{},
},
{
name: "Extensions over wire",
clientOpts: func() *ClientOptions {
caps := &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: true},
}
caps.AddExtension("io.example/ext", map[string]any{"key": "value"})
return &ClientOptions{Capabilities: caps}
}(),
wantCapabilities: &ClientCapabilities{
Extensions: map[string]any{
"io.example/ext": map[string]any{"key": "value"},
},
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
},
},
}
for _, tc := range testCases {

View file

@ -13,6 +13,7 @@ package mcp
import (
"encoding/json"
"fmt"
"maps"
)
// Optional annotations for the client. The client can use annotations to inform
@ -192,7 +193,14 @@ type ClientCapabilities struct {
// [ClientCapabilities.clone].
// Experimental reports non-standard capabilities that the client supports.
// The caller should not modify the map after assigning it.
Experimental map[string]any `json:"experimental,omitempty"`
// Extensions reports extensions that the client supports.
// Keys are extension identifiers in "{vendor-prefix}/{extension-name}" format.
// Values are per-extension settings objects; use [ClientCapabilities.AddExtension]
// to ensure nil settings are normalized to empty objects.
// The caller should not modify the map or its values after assigning it.
Extensions map[string]any `json:"extensions,omitempty"`
// Roots describes the client's support for roots.
//
// Deprecated: use RootsV2. As described in #607, Roots should have been a
@ -211,9 +219,26 @@ type ClientCapabilities struct {
Elicitation *ElicitationCapabilities `json:"elicitation,omitempty"`
}
// clone returns a deep copy of the ClientCapabilities.
// AddExtension adds an extension with the given name and settings.
// If settings is nil, an empty map is used to ensure valid JSON serialization
// (the spec requires an object, not null).
// The settings map should not be modified after the call.
func (c *ClientCapabilities) AddExtension(name string, settings map[string]any) {
if c.Extensions == nil {
c.Extensions = make(map[string]any)
}
if settings == nil {
settings = map[string]any{}
}
c.Extensions[name] = settings
}
// clone returns a copy of the ClientCapabilities.
// Values in the Extensions and Experimental maps are shallow-copied.
func (c *ClientCapabilities) clone() *ClientCapabilities {
cp := *c
cp.Experimental = maps.Clone(c.Experimental)
cp.Extensions = maps.Clone(c.Extensions)
cp.RootsV2 = shallowClone(c.RootsV2)
if c.Sampling != nil {
x := *c.Sampling
@ -1516,7 +1541,14 @@ type ServerCapabilities struct {
// [ServerCapabilities.clone].
// Experimental reports non-standard capabilities that the server supports.
// The caller should not modify the map after assigning it.
Experimental map[string]any `json:"experimental,omitempty"`
// Extensions reports extensions that the server supports.
// Keys are extension identifiers in "{vendor-prefix}/{extension-name}" format.
// Values are per-extension settings objects; use [ServerCapabilities.AddExtension]
// to ensure nil settings are normalized to empty objects.
// The caller should not modify the map or its values after assigning it.
Extensions map[string]any `json:"extensions,omitempty"`
// Completions is present if the server supports argument autocompletion
// suggestions.
Completions *CompletionCapabilities `json:"completions,omitempty"`
@ -1530,9 +1562,26 @@ type ServerCapabilities struct {
Tools *ToolCapabilities `json:"tools,omitempty"`
}
// clone returns a deep copy of the ServerCapabilities.
// AddExtension adds an extension with the given name and settings.
// If settings is nil, an empty map is used to ensure valid JSON serialization
// (the spec requires an object, not null).
// The settings map should not be modified after the call.
func (c *ServerCapabilities) AddExtension(name string, settings map[string]any) {
if c.Extensions == nil {
c.Extensions = make(map[string]any)
}
if settings == nil {
settings = map[string]any{}
}
c.Extensions[name] = settings
}
// clone returns a copy of the ServerCapabilities.
// Values in the Extensions and Experimental maps are shallow-copied.
func (c *ServerCapabilities) clone() *ServerCapabilities {
cp := *c
cp.Experimental = maps.Clone(c.Experimental)
cp.Extensions = maps.Clone(c.Extensions)
cp.Completions = shallowClone(c.Completions)
cp.Logging = shallowClone(c.Logging)
cp.Prompts = shallowClone(c.Prompts)

View file

@ -441,6 +441,25 @@ func TestServerCapabilities(t *testing.T) {
Logging: &LoggingCapabilities{},
},
},
{
name: "extensions preserved",
configureServer: func(s *Server) {},
serverOpts: func() ServerOptions {
caps := &ServerCapabilities{
Logging: &LoggingCapabilities{},
}
caps.AddExtension("io.example/ext1", map[string]any{"key": "value"})
caps.AddExtension("io.example/ext2", nil)
return ServerOptions{Capabilities: caps}
}(),
wantCapabilities: &ServerCapabilities{
Extensions: map[string]any{
"io.example/ext1": map[string]any{"key": "value"},
"io.example/ext2": map[string]any{},
},
Logging: &LoggingCapabilities{},
},
},
}
for _, tc := range testCases {
@ -927,6 +946,23 @@ func TestServerCapabilitiesOverWire(t *testing.T) {
Tools: &ToolCapabilities{ListChanged: true},
},
},
{
name: "Extensions over wire",
serverOpts: func() *ServerOptions {
caps := &ServerCapabilities{
Logging: &LoggingCapabilities{},
}
caps.AddExtension("io.example/ext", map[string]any{"key": "value"})
return &ServerOptions{Capabilities: caps}
}(),
configureServer: func(s *Server) {},
wantCapabilities: &ServerCapabilities{
Extensions: map[string]any{
"io.example/ext": map[string]any{"key": "value"},
},
Logging: &LoggingCapabilities{},
},
},
}
for _, tc := range testCases {