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
17 changes: 17 additions & 0 deletions docs/warts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- Autogenerated by weave; DO NOT EDIT -->
# "Warts" - API oversights to reconsider for v2

This file collects a list of API oversights or rough edges that we've uncovered
post v1.0, both to address them a potential future v2, and to document their
current workarounds.

- `EventStore.Open` is unnecessary. This was an artifact of an earlier version
of the SDK where event persistence and delivery were combined.

**Workaround**: `Open` may be implemented as a no-op.

- `ClientCapabilities.Roots` should have been a distinguished struct pointer.
This was simply an oversight.

**Workaround**: the `ClientCapabilities.Roots.Supported` field is managed by
custom marshaling of `ClientCapabilities`.
1 change: 1 addition & 0 deletions internal/docs/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//go:generate weave -o ../../docs/protocol.md ./protocol.src.md
//go:generate weave -o ../../docs/client.md ./client.src.md
//go:generate weave -o ../../docs/server.md ./server.src.md
//go:generate weave -o ../../docs/warts.md ./warts.src.md
//go:generate weave -o ../../docs/troubleshooting.md ./troubleshooting.src.md

// The doc package generates the documentation at /doc, via go:generate.
Expand Down
16 changes: 16 additions & 0 deletions internal/docs/warts.src.md
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inclusion of this file is a great idea- but is warts part of an internal coding style? I'd prefer it called known_issues.md but that's just my 2¢.

WRT the content of the file, I'd suggest creating a PR with the inclusion of the file, and then update the content to add each entry in a different PR, instead of adding the file and the 2 entries here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! Yes, I had intended to change the name of this file -- "warts" is a term I've hear before, but isn't nice.

"known issues" is good, but makes me think of "bugs we haven't yet fixed" rather than "API decisions we can't revert". In #609 I went with "rough edges", but let me know what you think.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# "Warts" - API oversights to reconsider for v2

This file collects a list of API oversights or rough edges that we've uncovered
post v1.0, both to address them a potential future v2, and to document their
current workarounds.

- `EventStore.Open` is unnecessary. This was an artifact of an earlier version
of the SDK where event persistence and delivery were combined.

**Workaround**: `Open` may be implemented as a no-op.

- `ClientCapabilities.Roots` should have been a distinguished struct pointer.
This was simply an oversight.

**Workaround**: the `ClientCapabilities.Roots.Supported` field is managed by
custom marshaling of `ClientCapabilities`.
1 change: 1 addition & 0 deletions mcp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ type ClientSessionOptions struct{}

func (c *Client) capabilities() *ClientCapabilities {
caps := &ClientCapabilities{}
caps.Roots.Supported = true
caps.Roots.ListChanged = true
if c.opts.CreateMessageHandler != nil {
caps.Sampling = &SamplingCapabilities{}
Expand Down
14 changes: 8 additions & 6 deletions mcp/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,10 @@ func TestClientCapabilities(t *testing.T) {
name: "With initial capabilities",
configureClient: func(s *Client) {},
wantCapabilities: &ClientCapabilities{
Roots: struct {
ListChanged bool "json:\"listChanged,omitempty\""
}{ListChanged: true},
Roots: rootsCapabilities{
Supported: true,
ListChanged: true,
},
},
},
{
Expand All @@ -216,9 +217,10 @@ func TestClientCapabilities(t *testing.T) {
},
},
wantCapabilities: &ClientCapabilities{
Roots: struct {
ListChanged bool "json:\"listChanged,omitempty\""
}{ListChanged: true},
Roots: rootsCapabilities{
Supported: true,
ListChanged: true,
},
Sampling: &SamplingCapabilities{},
},
},
Expand Down
64 changes: 58 additions & 6 deletions mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,71 @@ func (x *CancelledParams) SetProgressToken(t any) { setProgressToken(x, t) }
type ClientCapabilities struct {
// Experimental, non-standard capabilities that the client supports.
Experimental map[string]any `json:"experimental,omitempty"`
// Present if the client supports listing roots.

// Roots reports roots capabilities.
//
// Due to an API oversight (#607), roots is a non-pointer. Check the
// Supported field to see if the roots capability is present
Roots struct {
// Suppported reports whether roots/list is supported. It works around an
// API oversight: roots capabilities should have been a distinguished type,
// similar to other capabilities.
Supported bool `json:"-"`
Copy link

@robbyt robbyt Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is added, then I prefer that the other capabilities structs have a similar Supported bool field added, to keep the API consistent. (or just remove this Supported workaround field, and break backwards compatibility, because it's a bug)

Right now, my server must assume the client supports Sampling if ClientCapabilities.Sampling != nil, which is fine, but for simplicity I would like all capability fields to behave similarly.


// Whether the client supports notifications for changes to the roots list.
ListChanged bool `json:"listChanged,omitempty"`
} `json:"roots,omitempty"`
// Present if the client supports sampling from an LLM.
} `json:"roots"`

// Sampling is present if the client supports sampling from an LLM.
Sampling *SamplingCapabilities `json:"sampling,omitempty"`
// Present if the client supports elicitation from the server.

// Elicitation is present if the client supports elicitation from the server.
Elicitation *ElicitationCapabilities `json:"elicitation,omitempty"`
}

// clientCapabilitiesV2 is a version of ClientCapabilities fixes
type clientCapabilitiesV2 struct {
Experimental map[string]any `json:"experimental,omitempty"`
Roots *rootsCapabilities `json:"roots,omitempty"`
Sampling *SamplingCapabilities `json:"sampling,omitempty"`
Elicitation *ElicitationCapabilities `json:"elicitation,omitempty"`
}

// See #607: rootsCapabilities is for internal use, fixing an API oversight.
type rootsCapabilities struct {
Supported bool `json:"-"`
ListChanged bool `json:"listChanged,omitempty"`
}

func (c ClientCapabilities) MarshalJSON() ([]byte, error) {
c2 := &clientCapabilitiesV2{
Experimental: c.Experimental,
Sampling: c.Sampling,
Elicitation: c.Elicitation,
}
if c.Roots.Supported {
c2.Roots = &rootsCapabilities{
ListChanged: c.Roots.ListChanged,
}
}
return json.Marshal(c2)
}

func (c *ClientCapabilities) UnmarshalJSON(data []byte) error {
var c2 clientCapabilitiesV2
if err := json.Unmarshal(data, &c2); err != nil {
return err
}
c.Experimental = c2.Experimental
c.Elicitation = c2.Elicitation
c.Sampling = c2.Sampling
if c2.Roots != nil {
c.Roots = *c2.Roots
c.Roots.Supported = true
}
return nil
}

type CompleteParamsArgument struct {
// The name of the argument
Name string `json:"name"`
Expand Down Expand Up @@ -1035,8 +1089,6 @@ type ResourceUpdatedNotificationParams struct {

func (*ResourceUpdatedNotificationParams) isParams() {}

// TODO(jba): add CompleteRequest and related types.

// A request from the server to elicit additional information from the user via the client.
type ElicitParams struct {
// This property is reserved by the protocol to allow clients and servers to
Expand Down
27 changes: 27 additions & 0 deletions mcp/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,30 @@ func TestContentUnmarshal(t *testing.T) {
var gotpm PromptMessage
roundtrip(pm, &gotpm)
}

func TestClientCapabilitiesJSON(t *testing.T) {
tests := []struct {
capabilities ClientCapabilities
want string
}{
{ClientCapabilities{}, "{}"},
{ClientCapabilities{Roots: rootsCapabilities{Supported: true}}, `{"roots":{}}`},
{ClientCapabilities{Roots: rootsCapabilities{Supported: true, ListChanged: true}}, `{"roots":{"listChanged":true}}`},
}
for _, test := range tests {
gotBytes, err := json.Marshal(test.capabilities)
if err != nil {
t.Fatalf("json.Marshal(%+v) failed: %v", test.capabilities, err)
}
if got := string(gotBytes); got != test.want {
t.Fatalf("json.Marshal(%+v) = %q, want %q", test.capabilities, got, test.want)
}
var caps2 ClientCapabilities
if err := json.Unmarshal(gotBytes, &caps2); err != nil {
t.Fatalf("json.Unmarshal failed: %v", err)
}
if diff := cmp.Diff(test.capabilities, caps2); diff != "" {
t.Errorf("Unmarshal mismatch (-original +unmarshaled):\n%s", diff)
}
}
}
Loading