mcp: add Capabilities fields to ServerOptions and ClientOptions (#713)

This allows users to configure default capabilities, disable defaults,
or suppress listChanged notifications by setting ListChanged: false.

Key changes:
- Add ServerOptions.Capabilities and ClientOptions.Capabilities fields.
- Merge user-provided capabilities with dynamically-added ones (tools,
  prompts, resources, sampling, elicitation).
- Check ListChanged before sending list-changed notifications.
- Deprecate HasPrompts, HasResources, HasTools on ServerOptions.
- Remove unreleased ElicitationModes field from ClientOptions.
- Add clone methods to capability structs using shallowClone helper.
- Add synctest-based tests for notification behavior (go1.25).
- Add TestClientCapabilitiesOverWire and TestServerCapabilitiesOverWire
- Document the new Capabilities API in docs/

Updates #706
This commit is contained in:
Robert Findley 2025-12-11 20:17:46 -05:00 • committed by GitHub
commit 3430e2242e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1071 additions and 57 deletions

View file

@ -4,6 +4,9 @@
1. [Roots](#roots)
1. [Sampling](#sampling)
1. [Elicitation](#elicitation)
1. [Capabilities](#capabilities)
1. [Capability inference](#capability-inference)
1. [Explicit capabilities](#explicit-capabilities)
## Roots
@ -130,7 +133,9 @@ allows servers to request user inputs. It is implemented in the SDK as follows:
**Client-side**: To add the `elicitation` capability to a client, set
[`ClientOptions.ElicitationHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ElicitationHandler).
The elicitation handler must return a result that matches the requested schema;
otherwise, elicitation returns an error.
otherwise, elicitation returns an error. If your handler supports [URL mode
elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-mode-elicitation-requests),
you must declare that capability explicitly (see [Capabilities](#capabilities))
**Server-side**: To use elicitation from the server, call
[`ServerSession.Elicit`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Elicit).
@ -171,3 +176,60 @@ func Example_elicitation() {
// Output: value
}
```
## Capabilities
Client capabilities are advertised to servers during the initialization
handshake. [By default](rough_edges.md), the SDK advertises the `logging`
capability. Additional capabilities are automatically added when server
features are added (e.g. via `AddTool`), or when handlers are set in the
`ServerOptions` struct (e.g., setting `CompletionHandler` adds the
`completions` capability), or may be configured explicitly.
### Capability inference
When handlers are set on `ClientOptions` (e.g., `CreateMessageHandler` or
`ElicitationHandler`), the corresponding capability is automatically added if
not already present, with a default configuration.
For elicitation, if the handler is set but no `Capabilities.Elicitation` is
specified, the client defaults to form elicitation. To enable URL elicitation
or both modes, [configure `Capabilities.Elicitation`
explicitly](#explicit-capabilities).
See the [`ClientCapabilities`
documentation](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientCapabilities)
for further details on inference.
### Explicit capabilities
To explicitly declare capabilities, or to override the [default inferred
capability](#capability-inference), set
[`ClientOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.Capabilities).
This sets the initial client capabilities, before any capabilities are added
based on configured handlers. If a capability is already present in
`Capabilities`, adding a handler will not change its configuration.
This allows you to:
- **Disable default capabilities**: Pass an empty `&ClientCapabilities{}` to
disable all defaults, including roots.
- **Disable listChanged notifications**: Set `ListChanged: false` on a
capability to prevent the client from sending list-changed notifications
when roots are added or removed.
- **Configure elicitation modes**: Specify which elicitation modes (form, URL)
the client supports.
```go
// Configure elicitation modes and disable roots.
client := mcp.NewClient(impl, &mcp.ClientOptions{
Capabilities: &mcp.ClientCapabilities{
Elicitation: &mcp.ElicitationCapabilities{
Form: &mcp.FormElicitationCapabilities{},
URL: &mcp.URLElicitationCapabilities{},
},
},
ElicitationHandler: handler,
})
```

View file

@ -36,3 +36,12 @@ v2.
**Workaround**: use `ClientCapabilities.RootsV2`, which aligns with the
semantics of other capability fields.
- Default capabilities should have been empty. Instead, servers default to
advertising `logging`, and clients default to advertising `roots` with
`listChanged: true`. This is confusing because a nil `Capabilities` field
does not mean "no capabilities".
**Workaround**: to advertise no capabilities, set
`ServerOptions.Capabilities` or `ClientOptions.Capabilities` to an empty
`&ServerCapabilities{}` or `&ClientCapabilities{}` respectively.

View file

@ -7,6 +7,9 @@
1. [Utilities](#utilities)
1. [Completion](#completion)
1. [Logging](#logging)
1. [Capabilities](#capabilities)
1. [Capability inference](#capability-inference)
1. [Explicit capabilities](#explicit-capabilities)
1. [Pagination](#pagination)
## Prompts
@ -535,6 +538,53 @@ func Example_logging() {
}
```
## Capabilities
Server capabilities are advertised to clients during the initialization
handshake. By default, the SDK advertises only the `logging` capability.
Additional capabilities are automatically added when features are registered
(e.g., adding a tool adds the `tools` capability).
### Capability inference
When features such as tools, prompts, or resources are added to the server
(e.g., via `Server.AddTool`), their capability is automatically inferred, with
default value `{listChanged:true}`. Similarly, if the
`ServerOptions.SubscribeHandler` or `ServerOptions.CompletionHandler` are set,
the corresponding capability is added.
### Explicit capabilities
To explicitly declare capabilities, or to override the [default inferred
capability](#capability-inference), set
[`ServerOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.Capabilities).
This sets the default server capabilities, before any capabilities are added
based on configured handlers. If a capability is already present as a field in
`Capabilities`, adding a feature or handler will not change its configuration.
This allows you to:
- **Disable default capabilities**: Pass an empty `&ServerCapabilities{}` to
disable all defaults, including logging.
- **Disable listChanged notifications**: Set `ListChanged: false` on a
capability to prevent the server from sending list-changed notifications
when features are added or removed.
- **Pre-declare capabilities**: Declare capabilities before features are
registered, useful for servers that load features dynamically.
```go
// Disable listChanged notifications for tools
server := mcp.NewServer(impl, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{
Logging: &mcp.LoggingCapabilities{},
Tools: &mcp.ToolCapabilities{ListChanged: false},
},
})
```
**Deprecated**: The `HasPrompts`, `HasResources`, and `HasTools` fields on
`ServerOptions` are deprecated. Use `Capabilities` instead.
### Pagination
Server-side feature lists may be

View file

@ -47,9 +47,68 @@ allows servers to request user inputs. It is implemented in the SDK as follows:
**Client-side**: To add the `elicitation` capability to a client, set
[`ClientOptions.ElicitationHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ElicitationHandler).
The elicitation handler must return a result that matches the requested schema;
otherwise, elicitation returns an error.
otherwise, elicitation returns an error. If your handler supports [URL mode
elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-mode-elicitation-requests),
you must declare that capability explicitly (see [Capabilities](#capabilities))
**Server-side**: To use elicitation from the server, call
[`ServerSession.Elicit`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Elicit).
%include ../../mcp/client_example_test.go elicitation -
## Capabilities
Client capabilities are advertised to servers during the initialization
handshake. [By default](rough_edges.md), the SDK advertises the `logging`
capability. Additional capabilities are automatically added when server
features are added (e.g. via `AddTool`), or when handlers are set in the
`ServerOptions` struct (e.g., setting `CompletionHandler` adds the
`completions` capability), or may be configured explicitly.
### Capability inference
When handlers are set on `ClientOptions` (e.g., `CreateMessageHandler` or
`ElicitationHandler`), the corresponding capability is automatically added if
not already present, with a default configuration.
For elicitation, if the handler is set but no `Capabilities.Elicitation` is
specified, the client defaults to form elicitation. To enable URL elicitation
or both modes, [configure `Capabilities.Elicitation`
explicitly](#explicit-capabilities).
See the [`ClientCapabilities`
documentation](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientCapabilities)
for further details on inference.
### Explicit capabilities
To explicitly declare capabilities, or to override the [default inferred
capability](#capability-inference), set
[`ClientOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.Capabilities).
This sets the initial client capabilities, before any capabilities are added
based on configured handlers. If a capability is already present in
`Capabilities`, adding a handler will not change its configuration.
This allows you to:
- **Disable default capabilities**: Pass an empty `&ClientCapabilities{}` to
disable all defaults, including roots.
- **Disable listChanged notifications**: Set `ListChanged: false` on a
capability to prevent the client from sending list-changed notifications
when roots are added or removed.
- **Configure elicitation modes**: Specify which elicitation modes (form, URL)
the client supports.
```go
// Configure elicitation modes and disable roots.
client := mcp.NewClient(impl, &mcp.ClientOptions{
Capabilities: &mcp.ClientCapabilities{
Elicitation: &mcp.ElicitationCapabilities{
Form: &mcp.FormElicitationCapabilities{},
URL: &mcp.URLElicitationCapabilities{},
},
},
ElicitationHandler: handler,
})
```

View file

@ -35,3 +35,12 @@ v2.
**Workaround**: use `ClientCapabilities.RootsV2`, which aligns with the
semantics of other capability fields.
- Default capabilities should have been empty. Instead, servers default to
advertising `logging`, and clients default to advertising `roots` with
`listChanged: true`. This is confusing because a nil `Capabilities` field
does not mean "no capabilities".
**Workaround**: to advertise no capabilities, set
`ServerOptions.Capabilities` or `ClientOptions.Capabilities` to an empty
`&ServerCapabilities{}` or `&ClientCapabilities{}` respectively.

View file

@ -243,6 +243,53 @@ Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotoc
%include ../../mcp/server_example_test.go logging -
## Capabilities
Server capabilities are advertised to clients during the initialization
handshake. By default, the SDK advertises only the `logging` capability.
Additional capabilities are automatically added when features are registered
(e.g., adding a tool adds the `tools` capability).
### Capability inference
When features such as tools, prompts, or resources are added to the server
(e.g., via `Server.AddTool`), their capability is automatically inferred, with
default value `{listChanged:true}`. Similarly, if the
`ServerOptions.SubscribeHandler` or `ServerOptions.CompletionHandler` are set,
the corresponding capability is added.
### Explicit capabilities
To explicitly declare capabilities, or to override the [default inferred
capability](#capability-inference), set
[`ServerOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.Capabilities).
This sets the default server capabilities, before any capabilities are added
based on configured handlers. If a capability is already present as a field in
`Capabilities`, adding a feature or handler will not change its configuration.
This allows you to:
- **Disable default capabilities**: Pass an empty `&ServerCapabilities{}` to
disable all defaults, including logging.
- **Disable listChanged notifications**: Set `ListChanged: false` on a
capability to prevent the server from sending list-changed notifications
when features are added or removed.
- **Pre-declare capabilities**: Declare capabilities before features are
registered, useful for servers that load features dynamically.
```go
// Disable listChanged notifications for tools
server := mcp.NewServer(impl, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{
Logging: &mcp.LoggingCapabilities{},
Tools: &mcp.ToolCapabilities{ListChanged: false},
},
})
```
**Deprecated**: The `HasPrompts`, `HasResources`, and `HasTools` fields on
`ServerOptions` are deprecated. Use `Capabilities` instead.
### Pagination
Server-side feature lists may be

View file

@ -0,0 +1,180 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
//go:build go1.25
package mcp
import (
"context"
"sync/atomic"
"testing"
"testing/synctest"
"time"
"github.com/google/jsonschema-go/jsonschema"
)
// TestServerListChangedNotifications verifies that listChanged notifications
// are correctly sent or suppressed based on capability configuration.
func TestServerListChangedNotifications(t *testing.T) {
tool := &Tool{Name: "test-tool", InputSchema: &jsonschema.Schema{Type: "object"}}
testCases := []struct {
name string
serverOpts *ServerOptions
wantNotifyCount int64
}{
{
name: "Default: notification sent",
serverOpts: nil,
wantNotifyCount: 1,
},
{
name: "ListChanged false: notification suppressed",
serverOpts: &ServerOptions{
Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: false},
},
},
wantNotifyCount: 0,
},
{
name: "ListChanged true: notification sent",
serverOpts: &ServerOptions{
Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: true},
},
},
wantNotifyCount: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx := context.Background()
// Create server.
impl := &Implementation{Name: "testServer", Version: "v1.0.0"}
server := NewServer(impl, tc.serverOpts)
// Track notifications.
var notifyCount atomic.Int64
// Connect client and server.
cTransport, sTransport := NewInMemoryTransports()
ss, err := server.Connect(ctx, sTransport, nil)
if err != nil {
t.Fatal(err)
}
defer ss.Close()
client := NewClient(&Implementation{Name: "testClient", Version: "v1.0.0"}, &ClientOptions{
ToolListChangedHandler: func(ctx context.Context, req *ToolListChangedRequest) {
notifyCount.Add(1)
},
})
cs, err := client.Connect(ctx, cTransport, nil)
if err != nil {
t.Fatal(err)
}
defer cs.Close()
// Add a tool, which may or may not trigger notification.
server.AddTool(tool, nil)
// Sleep an arbitrary time longer than the debounce delay (synctest
// makes this practical).
time.Sleep(1 * time.Second)
// Wait for all goroutines to be blocked (notification delivered).
synctest.Wait()
if got, want := notifyCount.Load(), tc.wantNotifyCount; got != want {
t.Errorf("notification count: got %d, want %d", got, want)
}
})
})
}
}
// TestClientListChangedNotifications verifies that roots listChanged notifications
// are correctly sent or suppressed based on client capability configuration.
func TestClientListChangedNotifications(t *testing.T) {
root := &Root{URI: "file:///test"}
testCases := []struct {
name string
clientOpts *ClientOptions
wantNotifyCount int64
}{
{
name: "Default: notification sent",
clientOpts: nil,
wantNotifyCount: 1,
},
{
name: "ListChanged false: notification suppressed",
clientOpts: &ClientOptions{
Capabilities: &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: false},
},
},
wantNotifyCount: 0,
},
{
name: "ListChanged true: notification sent",
clientOpts: &ClientOptions{
Capabilities: &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: true},
},
},
wantNotifyCount: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx := context.Background()
// Track notifications.
var notifyCount atomic.Int64
// Create server with roots list changed handler.
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, &ServerOptions{
RootsListChangedHandler: func(ctx context.Context, req *RootsListChangedRequest) {
notifyCount.Add(1)
},
})
// Connect client and server.
cTransport, sTransport := NewInMemoryTransports()
ss, err := server.Connect(ctx, sTransport, nil)
if err != nil {
t.Fatal(err)
}
defer ss.Close()
client := NewClient(&Implementation{Name: "testClient", Version: "v1.0.0"}, tc.clientOpts)
cs, err := client.Connect(ctx, cTransport, nil)
if err != nil {
t.Fatal(err)
}
defer cs.Close()
// Add a root, which may or may not trigger notification.
client.AddRoots(root)
// Wait for all goroutines to be blocked (notification delivered).
synctest.Wait()
if got, want := notifyCount.Load(), tc.wantNotifyCount; got != want {
t.Errorf("notification count: got %d, want %d", got, want)
}
})
})
}
}

View file

@ -63,17 +63,57 @@ func NewClient(impl *Implementation, opts *ClientOptions) *Client {
type ClientOptions struct {
// CreateMessageHandler handles incoming requests for sampling/createMessage.
//
// Setting CreateMessageHandler to a non-nil value causes the client to
// advertise the sampling capability.
// Setting CreateMessageHandler to a non-nil value automatically causes the
// client to advertise the sampling capability, with default value
// &SamplingCapabilities{}. If [ClientOptions.Capabilities] is set and has a
// non nil value for [ClientCapabilities.Sampling], that value overrides the
// inferred capability.
CreateMessageHandler func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error)
// ElicitationHandler handles incoming requests for elicitation/create.
//
// Setting ElicitationHandler to a non-nil value causes the client to
// advertise the elicitation capability.
// Setting ElicitationHandler to a non-nil value automatically causes the
// client to advertise the elicitation capability, with default value
// &ElicitationCapabilities{}. If [ClientOptions.Capabilities] is set and has
// a non nil value for [ClientCapabilities.ELicitattion], that value
// overrides the inferred capability.
ElicitationHandler func(context.Context, *ElicitRequest) (*ElicitResult, error)
// ElicitationModes specifies the elicitation modes supported by the client.
// If ElicitationHandler is set and ElicitationModes is empty, it defaults to ["form"].
ElicitationModes []string
// Capabilities optionally configures the client's default capabilities,
// before any capabilities are inferred from other configuration.
//
// If Capabilities is nil, the default client capabilities are
// {"roots":{"listChanged":true}}, for historical reasons. Setting
// Capabilities to a non-nil value overrides this default. As a special case,
// to work around #607, Capabilities.Roots is ignored: set
// Capabilities.RootsV2 to configure the roots capability. This allows the
// "roots" capability to be disabled entirely.
//
// For example:
// - To disable the "roots" capability, use &ClientCapabilities{}
// - To configure "roots", but disable "listChanged" notifications, use
// &ClientCapabilities{RootsV2:&RootCapabilities{}}.
//
// # Interaction with capability inference
//
// Sampling and elicitation capabilities are automatically added when their
// corresponding handlers are set, with the default value described at
// [ClientOptions.CreateMessageHandler] and
// [ClientOptions.ElicitationHandler]. If the Sampling or Elicitation fields
// are set in the Capabilities field, their values override the inferred
// value.
//
// For example, to to configure elicitation modes:
//
// Capabilities: &ClientCapabilities{
// Elicitation: &ElicitationCapabilities{
// Form: &FormElicitationCapabilities{},
// URL: &URLElicitationCapabilities{},
// },
// }
//
// Conversely, if Capabilities does not set a field (for example, if the
// Elicitation field is nil), the inferred elicitation capability will be
// used.
Capabilities *ClientCapabilities
// ElicitationCompleteHandler handles incoming notifications for notifications/elicitation/complete.
ElicitationCompleteHandler func(context.Context, *ElicitationCompleteNotificationRequest)
// Handlers for notifications from the server.
@ -129,27 +169,43 @@ type ClientSessionOptions struct {
protocolVersion string
}
func (c *Client) capabilities() *ClientCapabilities {
caps := &ClientCapabilities{}
// Due to an oversight (#607), roots require special handling.
caps.Roots.ListChanged = true
caps.RootsV2 = &RootCapabilities{
ListChanged: true,
}
if c.opts.CreateMessageHandler != nil {
caps.Sampling = &SamplingCapabilities{}
}
if c.opts.ElicitationHandler != nil {
caps.Elicitation = &ElicitationCapabilities{}
modes := c.opts.ElicitationModes
if len(modes) == 0 || slices.Contains(modes, "form") {
// Technically, the empty ElicitationCapabilities value is equivalent to
// {"form":{}} for backward compatibility, but we explicitly set the form
// capability.
caps.Elicitation.Form = &FormElicitationCapabilities{}
func (c *Client) capabilities(protocolVersion string) *ClientCapabilities {
// Start with user-provided capabilities as defaults, or use SDK defaults.
var caps *ClientCapabilities
if c.opts.Capabilities != nil {
// Deep copy the user-provided capabilities to avoid mutation.
caps = c.opts.Capabilities.clone()
} else {
// SDK defaults: roots with listChanged.
// (this was the default behavior at v1.0.0, and so cannot be changed)
caps = &ClientCapabilities{
RootsV2: &RootCapabilities{
ListChanged: true,
},
}
if slices.Contains(modes, "url") {
caps.Elicitation.URL = &URLElicitationCapabilities{}
}
// Sync Roots from RootsV2 for backward compatibility (#607).
if caps.RootsV2 != nil {
caps.Roots = *caps.RootsV2
}
// Augment with sampling capability if handler is set.
if c.opts.CreateMessageHandler != nil {
if caps.Sampling == nil {
caps.Sampling = &SamplingCapabilities{}
}
}
// Augment with elicitation capability if handler is set.
if c.opts.ElicitationHandler != nil {
if caps.Elicitation == nil {
caps.Elicitation = &ElicitationCapabilities{}
// Form elicitation was added in 2025-11-25; for older versions,
// {} is treated the same as {"form":{}}.
if protocolVersion >= protocolVersion20251125 {
caps.Elicitation.Form = &FormElicitationCapabilities{}
}
}
}
return caps
@ -175,7 +231,7 @@ func (c *Client) Connect(ctx context.Context, t Transport, opts *ClientSessionOp
params := &InitializeParams{
ProtocolVersion: protocolVersion,
ClientInfo: c.impl,
Capabilities: c.capabilities(),
Capabilities: c.capabilities(protocolVersion),
}
req := &InitializeRequest{Session: cs, Params: params}
res, err := handleSend[*InitializeResult](ctx, methodInitialize, req)
@ -345,12 +401,38 @@ func changeAndNotify[P Params](c *Client, notification string, params P, change
// Lock for the change, but not for the notification.
c.mu.Lock()
if change() {
sessions = slices.Clone(c.sessions)
// Check if listChanged is enabled for this notification type.
if c.shouldSendListChangedNotification(notification) {
sessions = slices.Clone(c.sessions)
}
}
c.mu.Unlock()
notifySessions(sessions, notification, params, c.logger)
}
// shouldSendListChangedNotification checks if the client's capabilities allow
// sending the given list-changed notification.
func (c *Client) shouldSendListChangedNotification(notification string) bool {
// Get effective capabilities (considering user-provided defaults).
caps := c.opts.Capabilities
switch notification {
case notificationRootsListChanged:
// If user didn't specify capabilities, default behavior sends notifications.
if caps == nil {
return true
}
// Check RootsV2 first (preferred), then fall back to Roots.
if caps.RootsV2 != nil {
return caps.RootsV2.ListChanged
}
return caps.Roots.ListChanged
default:
// Unknown notification, allow by default.
return true
}
}
func (c *Client) listRoots(_ context.Context, req *ListRootsRequest) (*ListRootsResult, error) {
c.mu.Lock()
defer c.mu.Unlock()

View file

@ -196,10 +196,11 @@ func TestClientCapabilities(t *testing.T) {
name string
configureClient func(s *Client)
clientOpts ClientOptions
protocolVersion string // defaults to latestProtocolVersion if empty
wantCapabilities *ClientCapabilities
}{
{
name: "With initial capabilities",
name: "default",
configureClient: func(s *Client) {},
wantCapabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
@ -207,7 +208,7 @@ func TestClientCapabilities(t *testing.T) {
},
},
{
name: "With sampling",
name: "with sampling",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
CreateMessageHandler: func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) {
@ -221,14 +222,14 @@ func TestClientCapabilities(t *testing.T) {
},
},
{
name: "With form elicitation",
name: "with elicitation",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
ElicitationModes: []string{"form"},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return nil, nil
},
},
protocolVersion: protocolVersion20251125,
wantCapabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
@ -238,10 +239,31 @@ func TestClientCapabilities(t *testing.T) {
},
},
{
name: "With URL elicitation",
name: "with elicitation (old protocol)",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
ElicitationModes: []string{"url"},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return nil, nil
},
},
protocolVersion: protocolVersion20250618,
wantCapabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{},
},
},
{
name: "with URL elicitation",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
Capabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return nil, nil
},
@ -255,10 +277,17 @@ func TestClientCapabilities(t *testing.T) {
},
},
{
name: "With both form and URL elicitation",
name: "with form and URL elicitation",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
ElicitationModes: []string{"form", "url"},
Capabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
Form: &FormElicitationCapabilities{},
URL: &URLElicitationCapabilities{},
},
},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return nil, nil
},
@ -272,16 +301,190 @@ func TestClientCapabilities(t *testing.T) {
},
},
},
{
name: "no capabilities",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
Capabilities: &ClientCapabilities{},
},
wantCapabilities: &ClientCapabilities{},
},
{
name: "no roots",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
Capabilities: &ClientCapabilities{
Sampling: &SamplingCapabilities{},
},
},
wantCapabilities: &ClientCapabilities{
Sampling: &SamplingCapabilities{},
},
},
{
name: "roots-no list",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
Capabilities: &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: false},
},
},
wantCapabilities: &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: false},
},
},
{
name: "custom capabilities with sampling",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
Capabilities: &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: true},
},
CreateMessageHandler: func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) {
return nil, nil
},
},
wantCapabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Sampling: &SamplingCapabilities{},
},
},
{
name: "elicitation override",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
Capabilities: &ClientCapabilities{
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return nil, nil
},
},
wantCapabilities: &ClientCapabilities{
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
},
},
{
name: "custom capabilities with experimental",
configureClient: func(s *Client) {},
clientOpts: ClientOptions{
Capabilities: &ClientCapabilities{
Experimental: map[string]any{"custom": "value"},
RootsV2: &RootCapabilities{ListChanged: true},
},
},
wantCapabilities: &ClientCapabilities{
Experimental: map[string]any{"custom": "value"},
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewClient(testImpl, &tc.clientOpts)
tc.configureClient(client)
gotCapabilities := client.capabilities()
protocolVersion := tc.protocolVersion
if protocolVersion == "" {
protocolVersion = latestProtocolVersion
}
gotCapabilities := client.capabilities(protocolVersion)
if diff := cmp.Diff(tc.wantCapabilities, gotCapabilities); diff != "" {
t.Errorf("capabilities() mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestClientCapabilitiesOverWire(t *testing.T) {
testCases := []struct {
name string
clientOpts *ClientOptions
wantCapabilities *ClientCapabilities
}{
{
name: "Default capabilities",
clientOpts: nil,
wantCapabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
},
},
{
name: "Custom Capabilities with roots listChanged false",
clientOpts: &ClientOptions{
Capabilities: &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: false},
},
},
wantCapabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: false},
RootsV2: &RootCapabilities{ListChanged: false},
},
},
{
name: "Dynamic sampling capability",
clientOpts: &ClientOptions{
Capabilities: &ClientCapabilities{
RootsV2: &RootCapabilities{ListChanged: true},
},
CreateMessageHandler: func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) {
return nil, nil
},
},
wantCapabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Sampling: &SamplingCapabilities{},
},
},
{
name: "Empty capabilities disables defaults",
clientOpts: &ClientOptions{
Capabilities: &ClientCapabilities{},
},
wantCapabilities: &ClientCapabilities{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
// Create client.
impl := &Implementation{Name: "testClient", Version: "v1.0.0"}
client := NewClient(impl, tc.clientOpts)
// Connect client and server.
cTransport, sTransport := NewInMemoryTransports()
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
ss, err := server.Connect(ctx, sTransport, nil)
if err != nil {
t.Fatal(err)
}
defer ss.Close()
cs, err := client.Connect(ctx, cTransport, nil)
if err != nil {
t.Fatal(err)
}
defer cs.Close()
// Check that the server received the expected capabilities.
initParams := ss.InitializeParams()
if initParams == nil {
t.Fatal("InitializeParams is nil")
}
if diff := cmp.Diff(tc.wantCapabilities, initParams.Capabilities); diff != "" {
t.Errorf("Capabilities mismatch (-want +got):\n%s", diff)
}
})
}
}

View file

@ -105,7 +105,13 @@ func TestElicitationURLMode(t *testing.T) {
defer ss.Close()
c := NewClient(testImpl, &ClientOptions{
ElicitationModes: []string{"url"},
Capabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
},
ElicitationHandler: tc.handler,
})
cs, err := c.Connect(ctx, ct, nil)
@ -143,7 +149,13 @@ func TestElicitationCompleteNotification(t *testing.T) {
var elicitationCompleteCh = make(chan *ElicitationCompleteParams, 1)
c := NewClient(testImpl, &ClientOptions{
ElicitationModes: []string{"url"},
Capabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "accept"}, nil
},

View file

@ -213,7 +213,13 @@ func TestURLElicitationRequired(t *testing.T) {
// Create client with elicitation handler and middleware.
client := NewClient(testImpl, &ClientOptions{
ElicitationModes: []string{"url"},
Capabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
},
ElicitationHandler: func(ctx context.Context, req *ElicitRequest) (*ElicitResult, error) {
elicitCalled = true
elicitURL = req.Params.URL

View file

@ -188,6 +188,10 @@ type RootCapabilities struct {
// this schema, but this is not a closed set: any client can define its own,
// additional capabilities.
type ClientCapabilities struct {
// NOTE: any addition to ClientCapabilities must also be reflected in
// [ClientCapabilities.clone].
// Experimental reports non-standard capabilities that the client supports.
Experimental map[string]any `json:"experimental,omitempty"`
// Roots describes the client's support for roots.
@ -200,7 +204,7 @@ type ClientCapabilities struct {
// changes to the roots list.
ListChanged bool `json:"listChanged,omitempty"`
} `json:"roots,omitempty"`
// RootsV2 is present if the client supports roots.
// RootsV2 is present if the client supports roots. When capabilities are explicitly configured via [ClientOptions.Capabilities]
RootsV2 *RootCapabilities `json:"-"`
// Sampling is present if the client supports sampling from an LLM.
Sampling *SamplingCapabilities `json:"sampling,omitempty"`
@ -208,6 +212,29 @@ type ClientCapabilities struct {
Elicitation *ElicitationCapabilities `json:"elicitation,omitempty"`
}
// clone returns a deep copy of the ClientCapabilities.
func (c *ClientCapabilities) clone() *ClientCapabilities {
cp := *c
cp.RootsV2 = shallowClone(c.RootsV2)
cp.Sampling = shallowClone(c.Sampling)
if c.Elicitation != nil {
x := *c.Elicitation
x.Form = shallowClone(c.Elicitation.Form)
x.URL = shallowClone(c.Elicitation.URL)
cp.Elicitation = &x
}
return &cp
}
// shallowClone returns a shallow clone of *p, or nil if p is nil.
func shallowClone[T any](p *T) *T {
if p == nil {
return nil
}
x := *p
return &x
}
func (c *ClientCapabilities) toV2() *clientCapabilitiesV2 {
return &clientCapabilitiesV2{
ClientCapabilities: *c,
@ -1259,6 +1286,10 @@ type ToolCapabilities struct {
// ServerCapabilities describes capabilities that a server supports.
type ServerCapabilities struct {
// NOTE: any addition to ServerCapabilities must also be reflected in
// [ServerCapabilities.clone].
// Experimental reports non-standard capabilities that the server supports.
Experimental map[string]any `json:"experimental,omitempty"`
// Completions is present if the server supports argument autocompletion
@ -1274,6 +1305,17 @@ type ServerCapabilities struct {
Tools *ToolCapabilities `json:"tools,omitempty"`
}
// clone returns a deep copy of the ServerCapabilities.
func (c *ServerCapabilities) clone() *ServerCapabilities {
cp := *c
cp.Completions = shallowClone(c.Completions)
cp.Logging = shallowClone(c.Logging)
cp.Prompts = shallowClone(c.Prompts)
cp.Resources = shallowClone(c.Resources)
cp.Tools = shallowClone(c.Tools)
return &cp
}
const (
methodCallTool = "tools/call"
notificationCancelled = "notifications/cancelled"

View file

@ -81,14 +81,51 @@ type ServerOptions struct {
SubscribeHandler func(context.Context, *SubscribeRequest) error
// Function called when a client session unsubscribes from a resource.
UnsubscribeHandler func(context.Context, *UnsubscribeRequest) error
// Capabilities optionally configures the server's default capabilities,
// before any capabilities are inferred from other configuration or server
// features.
//
// If Capabilities is nil, the default server capabilities are {"logging":{}},
// for historical reasons. Setting Capabilities to a non-nil value overrides
// this default. For example, setting Capabilities to `&ServerCapabilities{}`
// disables the logging capability.
//
// # Interaction with capability inference
//
// "tools", "prompts", and "resources" capabilities are automatically added when
// tools, prompts, or resources are added to the server (for example, via
// [Server.AddPrompt]), with default value `{"listChanged":true}`. Similarly,
// if the [ClientOptions.SubscribeHandler] or
// [ClientOptions.CompletionHandler] are set, the inferred capabilities are
// adjusted accordingly.
//
// Any non-nil field in Capabilities overrides the inferred value.
// For example:
//
// - To advertise the "tools" capability, even if no tools are added, set
// Capabilities.Tools to &ToolCapabilities{ListChanged:true}.
// - To disable tool list notifications, set Capabilities.Tools to
// &ToolCapabilities{}.
//
// Conversely, if Capabilities does not set a field (for example, if the
// Prompts field is nil), the inferred capability will be used.
Capabilities *ServerCapabilities
// If true, advertises the prompts capability during initialization,
// even if no prompts have been registered.
//
// Deprecated: Use Capabilities instead.
HasPrompts bool
// If true, advertises the resources capability during initialization,
// even if no resources have been registered.
//
// Deprecated: Use Capabilities instead.
HasResources bool
// If true, advertises the tools capability during initialization,
// even if no tools have been registered.
//
// Deprecated: Use Capabilities instead.
HasTools bool
// GetSessionID provides the next session ID to use for an incoming request.
@ -465,24 +502,49 @@ func (s *Server) capabilities() *ServerCapabilities {
s.mu.Lock()
defer s.mu.Unlock()
caps := &ServerCapabilities{
Logging: &LoggingCapabilities{},
// Start with user-provided capabilities as defaults, or use SDK defaults.
var caps *ServerCapabilities
if s.opts.Capabilities != nil {
// Deep copy the user-provided capabilities to avoid mutation.
caps = s.opts.Capabilities.clone()
} else {
// SDK defaults: only logging capability.
caps = &ServerCapabilities{
Logging: &LoggingCapabilities{},
}
}
// Augment with tools capability if tools exist or legacy HasTools is set.
if s.opts.HasTools || s.tools.len() > 0 {
caps.Tools = &ToolCapabilities{ListChanged: true}
if caps.Tools == nil {
caps.Tools = &ToolCapabilities{ListChanged: true}
}
}
// Augment with prompts capability if prompts exist or legacy HasPrompts is set.
if s.opts.HasPrompts || s.prompts.len() > 0 {
caps.Prompts = &PromptCapabilities{ListChanged: true}
if caps.Prompts == nil {
caps.Prompts = &PromptCapabilities{ListChanged: true}
}
}
// Augment with resources capability if resources/templates exist or legacy HasResources is set.
if s.opts.HasResources || s.resources.len() > 0 || s.resourceTemplates.len() > 0 {
caps.Resources = &ResourceCapabilities{ListChanged: true}
if caps.Resources == nil {
caps.Resources = &ResourceCapabilities{ListChanged: true}
}
if s.opts.SubscribeHandler != nil {
caps.Resources.Subscribe = true
}
}
// Augment with completions capability if handler is set.
if s.opts.CompletionHandler != nil {
caps.Completions = &CompletionCapabilities{}
if caps.Completions == nil {
caps.Completions = &CompletionCapabilities{}
}
}
return caps
}
@ -512,7 +574,7 @@ const notificationDelay = 10 * time.Millisecond
func (s *Server) changeAndNotify(notification string, change func() bool) {
s.mu.Lock()
defer s.mu.Unlock()
if change() {
if change() && s.shouldSendListChangedNotification(notification) {
// Reset the outstanding delayed call, if any.
if t := s.pendingNotifications[notification]; t == nil {
s.pendingNotifications[notification] = time.AfterFunc(notificationDelay, func() { s.notifySessions(notification) })
@ -532,6 +594,35 @@ func (s *Server) notifySessions(n string) {
notifySessions(sessions, n, changeNotificationParams[n], s.opts.Logger)
}
// shouldSendListChangedNotification checks if the server's capabilities allow
// sending the given list-changed notification.
func (s *Server) shouldSendListChangedNotification(notification string) bool {
// Get effective capabilities (considering user-provided defaults).
caps := s.opts.Capabilities
switch notification {
case notificationToolListChanged:
// If user didn't specify capabilities, default behavior sends notifications.
if caps == nil || caps.Tools == nil {
return true
}
return caps.Tools.ListChanged
case notificationPromptListChanged:
if caps == nil || caps.Prompts == nil {
return true
}
return caps.Prompts.ListChanged
case notificationResourceListChanged:
if caps == nil || caps.Resources == nil {
return true
}
return caps.Resources.ListChanged
default:
// Unknown notification, allow by default.
return true
}
}
// Sessions returns an iterator that yields the current set of server sessions.
//
// There is no guarantee that the iterator observes sessions that are added or

View file

@ -245,14 +245,14 @@ func TestServerCapabilities(t *testing.T) {
wantCapabilities *ServerCapabilities
}{
{
name: "No capabilities",
name: "no capabilities",
configureServer: func(s *Server) {},
wantCapabilities: &ServerCapabilities{
Logging: &LoggingCapabilities{},
},
},
{
name: "With prompts",
name: "with prompts",
configureServer: func(s *Server) {
s.AddPrompt(&Prompt{Name: "p"}, nil)
},
@ -262,7 +262,7 @@ func TestServerCapabilities(t *testing.T) {
},
},
{
name: "With resources",
name: "with resources",
configureServer: func(s *Server) {
s.AddResource(&Resource{URI: "file:///r"}, nil)
},
@ -272,7 +272,7 @@ func TestServerCapabilities(t *testing.T) {
},
},
{
name: "With resource templates",
name: "with resource templates",
configureServer: func(s *Server) {
s.AddResourceTemplate(&ResourceTemplate{URITemplate: "file:///rt"}, nil)
},
@ -282,7 +282,7 @@ func TestServerCapabilities(t *testing.T) {
},
},
{
name: "With resource subscriptions",
name: "with resource subscriptions",
configureServer: func(s *Server) {
s.AddResourceTemplate(&ResourceTemplate{URITemplate: "file:///rt"}, nil)
},
@ -300,7 +300,7 @@ func TestServerCapabilities(t *testing.T) {
},
},
{
name: "With tools",
name: "with tools",
configureServer: func(s *Server) {
s.AddTool(tool, nil)
},
@ -310,7 +310,7 @@ func TestServerCapabilities(t *testing.T) {
},
},
{
name: "With completions",
name: "with completions",
configureServer: func(s *Server) {},
serverOpts: ServerOptions{
CompletionHandler: func(context.Context, *CompleteRequest) (*CompleteResult, error) {
@ -323,7 +323,7 @@ func TestServerCapabilities(t *testing.T) {
},
},
{
name: "With all capabilities",
name: "all capabilities",
configureServer: func(s *Server) {
s.AddPrompt(&Prompt{Name: "p"}, nil)
s.AddResource(&Resource{URI: "file:///r"}, nil)
@ -350,7 +350,7 @@ func TestServerCapabilities(t *testing.T) {
},
},
{
name: "With initial capabilities",
name: "has features",
configureServer: func(s *Server) {},
serverOpts: ServerOptions{
HasPrompts: true,
@ -364,6 +364,83 @@ func TestServerCapabilities(t *testing.T) {
Tools: &ToolCapabilities{ListChanged: true},
},
},
{
name: "empty capabilities",
configureServer: func(s *Server) {},
serverOpts: ServerOptions{
Capabilities: &ServerCapabilities{},
},
wantCapabilities: &ServerCapabilities{},
},
{
name: "no logging",
configureServer: func(s *Server) {},
serverOpts: ServerOptions{
Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: true},
},
},
wantCapabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: true},
},
},
{
name: "no list",
configureServer: func(s *Server) {},
serverOpts: ServerOptions{
Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: false},
Prompts: &PromptCapabilities{ListChanged: false},
},
},
wantCapabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: false},
Prompts: &PromptCapabilities{ListChanged: false},
},
},
{
name: "adding tools-list",
configureServer: func(s *Server) {
s.AddTool(tool, nil)
},
serverOpts: ServerOptions{
Capabilities: &ServerCapabilities{
Logging: &LoggingCapabilities{},
},
},
wantCapabilities: &ServerCapabilities{
Logging: &LoggingCapabilities{},
Tools: &ToolCapabilities{ListChanged: true},
},
},
{
name: "adding tools-no list",
configureServer: func(s *Server) {
s.AddTool(tool, nil)
},
serverOpts: ServerOptions{
Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: false},
},
},
wantCapabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: false},
},
},
{
name: "experimental preserved",
configureServer: func(s *Server) {},
serverOpts: ServerOptions{
Capabilities: &ServerCapabilities{
Experimental: map[string]any{"custom": "value"},
Logging: &LoggingCapabilities{},
},
},
wantCapabilities: &ServerCapabilities{
Experimental: map[string]any{"custom": "value"},
Logging: &LoggingCapabilities{},
},
},
}
for _, tc := range testCases {
@ -776,3 +853,88 @@ func TestToolForSchemas(t *testing.T) {
},
"")
}
// TestServerCapabilitiesOverWire verifies that server capabilities are
// correctly sent over the wire during initialization.
func TestServerCapabilitiesOverWire(t *testing.T) {
tool := &Tool{Name: "test-tool", InputSchema: &jsonschema.Schema{Type: "object"}}
testCases := []struct {
name string
serverOpts *ServerOptions
configureServer func(s *Server)
wantCapabilities *ServerCapabilities
}{
{
name: "Default capabilities",
serverOpts: nil,
configureServer: func(s *Server) {},
wantCapabilities: &ServerCapabilities{
Logging: &LoggingCapabilities{},
},
},
{
name: "Custom Capabilities with tools",
serverOpts: &ServerOptions{
Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: false},
},
},
configureServer: func(s *Server) {},
wantCapabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: false},
},
},
{
name: "Dynamic tool capability",
serverOpts: &ServerOptions{
Capabilities: &ServerCapabilities{
Logging: &LoggingCapabilities{},
},
},
configureServer: func(s *Server) {
s.AddTool(tool, nil)
},
wantCapabilities: &ServerCapabilities{
Logging: &LoggingCapabilities{},
Tools: &ToolCapabilities{ListChanged: true},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
// Create server.
impl := &Implementation{Name: "testServer", Version: "v1.0.0"}
server := NewServer(impl, tc.serverOpts)
tc.configureServer(server)
// Connect client and server.
cTransport, sTransport := NewInMemoryTransports()
ss, err := server.Connect(ctx, sTransport, nil)
if err != nil {
t.Fatal(err)
}
defer ss.Close()
client := NewClient(&Implementation{Name: "testClient", Version: "v1.0.0"}, nil)
cs, err := client.Connect(ctx, cTransport, nil)
if err != nil {
t.Fatal(err)
}
defer cs.Close()
// Check that the client received the expected capabilities.
initResult := cs.InitializeResult()
if initResult == nil {
t.Fatal("InitializeResult is nil")
}
if diff := cmp.Diff(tc.wantCapabilities, initResult.Capabilities); diff != "" {
t.Errorf("Capabilities mismatch (-want +got):\n%s", diff)
}
})
}
}