Skip to content

Commit 3da831b

Browse files
authored
mcp: move to 2025-06-18 schema (#8)
* mcp: move to 2025-06-18 schema Add types and fields for the latest MCP schema. The main challenge here was dealing with the presence of _meta fields on many types, like Tool, Prompt and Resource. - Distinguish between the _meta field of a request's params, which includes a progress token, from the _meta field on other types, which is simply a map. - Redefine GetMeta to return a pointer to the map. We wanted to avoid that, but I don't see a clean way to do so. An alternative would be to have GetMeta return a map, and SetMeta to set it. - Provide GetProgressToken for requests only (not notifications or other types). - Still to do: deal with the meta and annotation fields of ContentBlock, which are currently duplicated for Resource and ResourceContents. * Meta and progress token redesign
1 parent 735ad51 commit 3da831b

20 files changed

+372
-249
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ type HiParams struct {
9494

9595
func SayHi(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) {
9696
return &mcp.CallToolResultFor[any]{
97-
Content: []*mcp.Content{mcp.NewTextContent("Hi " + params.Name)},
97+
Content: []*mcp.ContentBlock{mcp.NewTextContent("Hi " + params.Name)},
9898
}, nil
9999
}
100100

examples/hello/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type HiArgs struct {
2424

2525
func SayHi(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[HiArgs]) (*mcp.CallToolResultFor[struct{}], error) {
2626
return &mcp.CallToolResultFor[struct{}]{
27-
Content: []*mcp.Content{
27+
Content: []*mcp.ContentBlock{
2828
mcp.NewTextContent("Hi " + params.Name),
2929
},
3030
}, nil

examples/sse/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type SayHiParams struct {
2121

2222
func SayHi(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[SayHiParams]) (*mcp.CallToolResultFor[any], error) {
2323
return &mcp.CallToolResultFor[any]{
24-
Content: []*mcp.Content{
24+
Content: []*mcp.ContentBlock{
2525
mcp.NewTextContent("Hi " + params.Name),
2626
},
2727
}, nil

internal/readme/server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type HiParams struct {
1818

1919
func SayHi(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) {
2020
return &mcp.CallToolResultFor[any]{
21-
Content: []*mcp.Content{mcp.NewTextContent("Hi " + params.Name)},
21+
Content: []*mcp.ContentBlock{mcp.NewTextContent("Hi " + params.Name)},
2222
}, nil
2323
}
2424

mcp/cmd_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func TestCmdTransport(t *testing.T) {
7070
log.Fatal(err)
7171
}
7272
want := &mcp.CallToolResult{
73-
Content: []*mcp.Content{{Type: "text", Text: "Hi user"}},
73+
Content: []*mcp.ContentBlock{{Type: "text", Text: "Hi user"}},
7474
}
7575
if diff := cmp.Diff(want, got); diff != "" {
7676
t.Errorf("greet returned unexpected content (-want +got):\n%s", diff)

mcp/content.go

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,58 +10,67 @@ import (
1010
"fmt"
1111
)
1212

13-
// Content is the wire format for content.
14-
// It represents the protocol types TextContent, ImageContent, AudioContent
15-
// and EmbeddedResource.
16-
// Use [NewTextContent], [NewImageContent], [NewAudioContent] or [NewResourceContent]
17-
// to create one.
13+
// A ContentBlock is one of a TextContent, ImageContent, AudioContent
14+
// ResourceLink, or EmbeddedResource.
15+
// Use [NewTextContent], [NewImageContent], [NewAudioContent], [NewResourceLink]
16+
// or [NewResourceContents] to create one.
1817
//
19-
// The Type field must be one of "text", "image", "audio" or "resource". The
20-
// constructors above populate this field appropriately.
21-
// Although at most one of Text, Data, and Resource should be non-zero, consumers of Content
22-
// use the Type field to determine which value to use; values in the other fields are ignored.
23-
type Content struct {
24-
Type string `json:"type"`
25-
Text string `json:"text,omitempty"`
26-
MIMEType string `json:"mimeType,omitempty"`
27-
Data []byte `json:"data,omitempty"`
28-
Resource *ResourceContents `json:"resource,omitempty"`
29-
Annotations *Annotations `json:"annotations,omitempty"`
18+
// The Type field must be one of "text", "image", "audio", "resource_link" or "resource".
19+
// The constructors above populate this field appropriately.
20+
// Although at most one of Text, Data, ResourceLink and Resource should be non-zero,
21+
// consumers of ContentBlock use the Type field to determine which value to use;
22+
// values in the other fields are ignored.
23+
// TODO(jba,rfindley): rethink this type. Each kind (text, image, etc.) should have its own
24+
// meta and annotations, otherwise they're duplicated for Resource and ResourceContents.
25+
type ContentBlock struct {
26+
Meta map[string]any `json:"_meta,omitempty"`
27+
Type string `json:"type"`
28+
Text string `json:"text,omitempty"`
29+
MIMEType string `json:"mimeType,omitempty"`
30+
Data []byte `json:"data,omitempty"`
31+
ResourceLink *Resource `json:"resource_link,omitempty"`
32+
Resource *ResourceContents `json:"resource,omitempty"`
33+
Annotations *Annotations `json:"annotations,omitempty"`
3034
}
3135

32-
func (c *Content) UnmarshalJSON(data []byte) error {
33-
type wireContent Content // for naive unmarshaling
36+
func (c *ContentBlock) UnmarshalJSON(data []byte) error {
37+
type wireContent ContentBlock // for naive unmarshaling
3438
var c2 wireContent
3539
if err := json.Unmarshal(data, &c2); err != nil {
3640
return err
3741
}
3842
switch c2.Type {
39-
case "text", "image", "audio", "resource":
43+
case "text", "image", "audio", "resource", "resource_link":
4044
default:
4145
return fmt.Errorf("unrecognized content type %s", c.Type)
4246
}
43-
*c = Content(c2)
47+
*c = ContentBlock(c2)
4448
return nil
4549
}
4650

47-
// NewTextContent creates a [Content] with text.
48-
func NewTextContent(text string) *Content {
49-
return &Content{Type: "text", Text: text}
51+
// NewTextContent creates a [ContentBlock] with text.
52+
func NewTextContent(text string) *ContentBlock {
53+
return &ContentBlock{Type: "text", Text: text}
5054
}
5155

52-
// NewImageContent creates a [Content] with image data.
53-
func NewImageContent(data []byte, mimeType string) *Content {
54-
return &Content{Type: "image", Data: data, MIMEType: mimeType}
56+
// NewImageContent creates a [ContentBlock] with image data.
57+
func NewImageContent(data []byte, mimeType string) *ContentBlock {
58+
return &ContentBlock{Type: "image", Data: data, MIMEType: mimeType}
5559
}
5660

57-
// NewAudioContent creates a [Content] with audio data.
58-
func NewAudioContent(data []byte, mimeType string) *Content {
59-
return &Content{Type: "audio", Data: data, MIMEType: mimeType}
61+
// NewAudioContent creates a [ContentBlock] with audio data.
62+
func NewAudioContent(data []byte, mimeType string) *ContentBlock {
63+
return &ContentBlock{Type: "audio", Data: data, MIMEType: mimeType}
6064
}
6165

62-
// NewResourceContent creates a [Content] with an embedded resource.
63-
func NewResourceContent(resource *ResourceContents) *Content {
64-
return &Content{Type: "resource", Resource: resource}
66+
// NewResourceLink creates a [ContentBlock] with a [Resource].
67+
func NewResourceLink(r *Resource) *ContentBlock {
68+
return &ContentBlock{Type: "resource_link", ResourceLink: r}
69+
}
70+
71+
// NewResourceContents creates a [ContentBlock] with an embedded resource (a [ResourceContents]).
72+
func NewResourceContents(rc *ResourceContents) *ContentBlock {
73+
return &ContentBlock{Type: "resource", Resource: rc}
6574
}
6675

6776
// ResourceContents represents the union of the spec's {Text,Blob}ResourceContents types.
@@ -71,10 +80,11 @@ func NewResourceContent(resource *ResourceContents) *Content {
7180
// A ResourceContents is either a TextResourceContents or a BlobResourceContents.
7281
// Use [NewTextResourceContents] or [NextBlobResourceContents] to create one.
7382
type ResourceContents struct {
74-
URI string `json:"uri"` // resource location; must not be empty
75-
MIMEType string `json:"mimeType,omitempty"`
76-
Text string `json:"text"`
77-
Blob []byte `json:"blob,omitempty"` // if nil, then text; else blob
83+
Meta map[string]any `json:"_meta,omitempty"`
84+
URI string `json:"uri"` // resource location; must not be empty
85+
MIMEType string `json:"mimeType,omitempty"`
86+
Text string `json:"text"`
87+
Blob []byte `json:"blob,omitempty"` // if nil, then text; else blob
7888
}
7989

8090
func (r ResourceContents) MarshalJSON() ([]byte, error) {

mcp/content_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
func TestContent(t *testing.T) {
1616
tests := []struct {
17-
in *mcp.Content
17+
in *mcp.ContentBlock
1818
want string // json serialization
1919
}{
2020
{mcp.NewTextContent("hello"), `{"type":"text","text":"hello"}`},
@@ -27,13 +27,13 @@ func TestContent(t *testing.T) {
2727
`{"type":"audio","mimeType":"audio/wav","data":"YTFiMmMz"}`,
2828
},
2929
{
30-
mcp.NewResourceContent(
30+
mcp.NewResourceContents(
3131
mcp.NewTextResourceContents("file://foo", "text", "abc"),
3232
),
3333
`{"type":"resource","resource":{"uri":"file://foo","mimeType":"text","text":"abc"}}`,
3434
},
3535
{
36-
mcp.NewResourceContent(
36+
mcp.NewResourceContents(
3737
mcp.NewBlobResourceContents("file://foo", "image/png", []byte("a1b2c3")),
3838
),
3939
`{"type":"resource","resource":{"uri":"file://foo","mimeType":"image/png","blob":"YTFiMmMz"}}`,
@@ -48,7 +48,7 @@ func TestContent(t *testing.T) {
4848
if diff := cmp.Diff(test.want, string(got)); diff != "" {
4949
t.Errorf("json.Marshal(%v) mismatch (-want +got):\n%s", test.in, diff)
5050
}
51-
var out *mcp.Content
51+
var out *mcp.ContentBlock
5252
if err := json.Unmarshal(got, &out); err != nil {
5353
t.Fatal(err)
5454
}

mcp/example_progress_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ func Example_progressMiddleware() {
2323

2424
func addProgressToken[S mcp.Session](h mcp.MethodHandler[S]) mcp.MethodHandler[S] {
2525
return func(ctx context.Context, s S, method string, params mcp.Params) (result mcp.Result, err error) {
26-
params.GetMeta().ProgressToken = nextProgressToken.Add(1)
26+
if rp, ok := params.(mcp.RequestParams); ok {
27+
rp.SetProgressToken(nextProgressToken.Add(1))
28+
}
2729
return h(ctx, s, method, params)
2830
}
2931
}

mcp/features_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type SayHiParams struct {
2020

2121
func SayHi(ctx context.Context, cc *ServerSession, params *CallToolParamsFor[SayHiParams]) (*CallToolResultFor[any], error) {
2222
return &CallToolResultFor[any]{
23-
Content: []*Content{
23+
Content: []*ContentBlock{
2424
NewTextContent("Hi " + params.Name),
2525
},
2626
}, nil

mcp/mcp_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func sayHi(ctx context.Context, ss *ServerSession, params *CallToolParamsFor[hiP
3434
if err := ss.Ping(ctx, nil); err != nil {
3535
return nil, fmt.Errorf("ping failed: %v", err)
3636
}
37-
return &CallToolResultFor[any]{Content: []*Content{NewTextContent("hi " + params.Arguments.Name)}}, nil
37+
return &CallToolResultFor[any]{Content: []*ContentBlock{NewTextContent("hi " + params.Arguments.Name)}}, nil
3838
}
3939

4040
func TestEndToEnd(t *testing.T) {
@@ -195,7 +195,7 @@ func TestEndToEnd(t *testing.T) {
195195
t.Fatal(err)
196196
}
197197
wantHi := &CallToolResult{
198-
Content: []*Content{{Type: "text", Text: "hi user"}},
198+
Content: []*ContentBlock{{Type: "text", Text: "hi user"}},
199199
}
200200
if diff := cmp.Diff(wantHi, gotHi); diff != "" {
201201
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
@@ -212,7 +212,7 @@ func TestEndToEnd(t *testing.T) {
212212
}
213213
wantFail := &CallToolResult{
214214
IsError: true,
215-
Content: []*Content{{Type: "text", Text: errTestFailure.Error()}},
215+
Content: []*ContentBlock{{Type: "text", Text: errTestFailure.Error()}},
216216
}
217217
if diff := cmp.Diff(wantFail, gotFail); diff != "" {
218218
t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff)

0 commit comments

Comments
 (0)