Skip to content

Commit c36c0bb

Browse files
committed
mcp,design: revert 'content' back to an interface type
WIP DO NOT REVIEW DO NOT SUBMIT
1 parent a3730da commit c36c0bb

File tree

16 files changed

+240
-152
lines changed

16 files changed

+240
-152
lines changed

examples/hello/main.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ 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{
28-
mcp.NewTextContent("Hi " + params.Name),
27+
Content: []mcp.Content{
28+
&mcp.TextContent{Text: "Hi " + params.Name},
2929
},
3030
}, nil
3131
}
@@ -36,7 +36,7 @@ func PromptHi(ctx context.Context, ss *mcp.ServerSession, params *mcp.GetPromptP
3636
return &mcp.GetPromptResult{
3737
Description: "Code review prompt",
3838
Messages: []*mcp.PromptMessage{
39-
{Role: "user", Content: mcp.NewTextContent("Say hi to " + params.Arguments["name"])},
39+
{Role: "user", Content: &mcp.TextContent{Text: "Say hi to " + params.Arguments["name"]}},
4040
},
4141
}, nil
4242
}
@@ -93,6 +93,8 @@ func handleEmbeddedResource(_ context.Context, _ *mcp.ServerSession, params *mcp
9393
return nil, fmt.Errorf("no embedded resource named %q", key)
9494
}
9595
return &mcp.ReadResourceResult{
96-
Contents: []*mcp.ResourceContents{mcp.NewTextResourceContents(params.URI, "text/plain", text)},
96+
Contents: []*mcp.ResourceContents{
97+
{URI: params.URI, MIMEType: "text/plain", Text: text},
98+
},
9799
}, nil
98100
}

examples/sse/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ 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{
25-
mcp.NewTextContent("Hi " + params.Name),
24+
Content: []mcp.Content{
25+
&mcp.TextContent{Text: "Hi " + params.Name},
2626
},
2727
}, nil
2828
}

internal/readme/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func main() {
4040
log.Fatal("tool failed")
4141
}
4242
for _, c := range res.Content {
43-
log.Print(c.Text)
43+
log.Print(c.(*mcp.TextContent).Text)
4444
}
4545
}
4646

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.Content{&mcp.TextContent{Text: "Hi " + params.Name}},
2222
}, nil
2323
}
2424

mcp/cmd_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ 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.Content{
74+
&mcp.TextContent{Text: "Hi user"},
75+
},
7476
}
7577
if diff := cmp.Diff(want, got); diff != "" {
7678
t.Errorf("greet returned unexpected content (-want +got):\n%s", diff)

mcp/content.go

Lines changed: 106 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -6,123 +6,137 @@ package mcp
66

77
import (
88
"encoding/json"
9-
"errors"
109
"fmt"
1110
)
1211

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.
18-
//
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"`
12+
// A Content is a [TextContent], [ImageContent], [AudioContent] or
13+
// [EmbeddedResource].
14+
type Content interface {
15+
MarshalJSON() ([]byte, error)
16+
fromWire(*wireContent)
3017
}
3118

32-
func (c *Content) UnmarshalJSON(data []byte) error {
33-
type wireContent Content // for naive unmarshaling
34-
var c2 wireContent
35-
if err := json.Unmarshal(data, &c2); err != nil {
36-
return err
37-
}
38-
switch c2.Type {
39-
case "text", "image", "audio", "resource":
40-
default:
41-
return fmt.Errorf("unrecognized content type %s", c.Type)
42-
}
43-
*c = Content(c2)
44-
return nil
19+
// TextContent is a textual content.
20+
type TextContent struct {
21+
Text string
4522
}
4623

47-
// NewTextContent creates a [Content] with text.
48-
func NewTextContent(text string) *Content {
49-
return &Content{Type: "text", Text: text}
24+
func (c *TextContent) MarshalJSON() ([]byte, error) {
25+
return json.Marshal(&wireContent{Type: "text", Text: c.Text})
5026
}
5127

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}
28+
func (c *TextContent) fromWire(wire *wireContent) {
29+
c.Text = wire.Text
5530
}
5631

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}
32+
// ImageContent contains base64-encoded image data.
33+
type ImageContent struct {
34+
Data []byte // base64-encoded
35+
MIMEType string
6036
}
6137

62-
// NewResourceContent creates a [Content] with an embedded resource.
63-
func NewResourceContent(resource *ResourceContents) *Content {
64-
return &Content{Type: "resource", Resource: resource}
38+
func (c *ImageContent) MarshalJSON() ([]byte, error) {
39+
return json.Marshal(&wireContent{Type: "image", MIMEType: c.MIMEType, Data: c.Data})
6540
}
6641

67-
// ResourceContents represents the union of the spec's {Text,Blob}ResourceContents types.
68-
// See https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts#L524-L551
69-
// for the inheritance structure.
42+
func (c *ImageContent) fromWire(wire *wireContent) {
43+
c.MIMEType = wire.MIMEType
44+
c.Data = wire.Data
45+
}
46+
47+
// AudioContent contains base64-encoded audio data.
48+
type AudioContent struct {
49+
Data []byte
50+
MIMEType string
51+
}
52+
53+
func (c AudioContent) MarshalJSON() ([]byte, error) {
54+
return json.Marshal(&wireContent{Type: "audio", MIMEType: c.MIMEType, Data: c.Data})
55+
}
56+
57+
func (c *AudioContent) fromWire(wire *wireContent) {
58+
c.MIMEType = wire.MIMEType
59+
c.Data = wire.Data
60+
}
61+
62+
// EmbeddedResource contains embedded resources.
63+
type EmbeddedResource struct {
64+
Resource *ResourceContents
65+
}
7066

71-
// A ResourceContents is either a TextResourceContents or a BlobResourceContents.
72-
// Use [NewTextResourceContents] or [NextBlobResourceContents] to create one.
67+
func (r *EmbeddedResource) MarshalJSON() ([]byte, error) {
68+
return json.Marshal(&wireContent{Type: "resource", Resource: r.Resource})
69+
}
70+
71+
func (c *EmbeddedResource) fromWire(wire *wireContent) {
72+
c.Resource = wire.Resource
73+
}
74+
75+
// FIXME: doc
7376
type ResourceContents struct {
74-
URI string `json:"uri"` // resource location; must not be empty
77+
URI string `json:"uri,"`
7578
MIMEType string `json:"mimeType,omitempty"`
76-
Text string `json:"text"`
77-
Blob []byte `json:"blob,omitempty"` // if nil, then text; else blob
79+
Text string `json:"text,omitempty"`
80+
Blob []byte `json:"blob,omitzero"`
7881
}
7982

80-
func (r ResourceContents) MarshalJSON() ([]byte, error) {
81-
// If we could assume Go 1.24, we could use omitzero for Blob and avoid this method.
82-
if r.URI == "" {
83-
return nil, errors.New("ResourceContents missing URI")
84-
}
85-
if r.Blob == nil {
86-
// Text. Marshal normally.
87-
type wireResourceContents ResourceContents // (lacks MarshalJSON method)
88-
return json.Marshal((wireResourceContents)(r))
89-
}
90-
// Blob.
91-
if r.Text != "" {
92-
return nil, errors.New("ResourceContents has non-zero Text and Blob fields")
93-
}
94-
// r.Blob may be the empty slice, so marshal with an alternative definition.
95-
br := struct {
96-
URI string `json:"uri,omitempty"`
97-
MIMEType string `json:"mimeType,omitempty"`
98-
Blob []byte `json:"blob"`
99-
}{
100-
URI: r.URI,
101-
MIMEType: r.MIMEType,
102-
Blob: r.Blob,
103-
}
104-
return json.Marshal(br)
83+
// wireContent is the wire format for content.
84+
// It represents the protocol types TextContent, ImageContent, AudioContent
85+
// and EmbeddedResource.
86+
// The Type field distinguishes them. In the protocol, each type has a constant
87+
// value for the field.
88+
// At most one of Text, Data, and Resource is non-zero.
89+
type wireContent struct {
90+
Type string `json:"type"`
91+
Text string `json:"text,omitempty"`
92+
MIMEType string `json:"mimeType,omitempty"`
93+
Data []byte `json:"data,omitempty"`
94+
Resource *ResourceContents `json:"resource,omitempty"`
95+
Annotations *Annotations `json:"annotations,omitempty"`
96+
}
97+
98+
// A wireResource is either a TextResourceContents or a BlobResourceContents.
99+
// See https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts#L524-L551
100+
// for the inheritance structure.
101+
// If Blob is nil, this is a TextResourceContents; otherwise it's a BlobResourceContents.
102+
//
103+
// The URI field describes the resource location.
104+
type wireResource struct {
105105
}
106106

107-
// NewTextResourceContents returns a [ResourceContents] containing text.
108-
func NewTextResourceContents(uri, mimeType, text string) *ResourceContents {
109-
return &ResourceContents{
110-
URI: uri,
111-
MIMEType: mimeType,
112-
Text: text,
113-
// Blob is nil, indicating this is a TextResourceContents.
107+
func contentsFromWire(wires []*wireContent, allow map[string]bool) ([]Content, error) {
108+
var blocks []Content
109+
for _, wire := range wires {
110+
block, err := contentFromWire(wire, allow)
111+
if err != nil {
112+
return nil, err
113+
}
114+
blocks = append(blocks, block)
114115
}
116+
return blocks, nil
115117
}
116118

117-
// NewBlobResourceContents returns a [ResourceContents] containing a byte slice.
118-
func NewBlobResourceContents(uri, mimeType string, blob []byte) *ResourceContents {
119-
// The only way to distinguish text from blob is a non-nil Blob field.
120-
if blob == nil {
121-
blob = []byte{}
119+
func contentFromWire(wire *wireContent, allow map[string]bool) (Content, error) {
120+
if allow != nil && !allow[wire.Type] {
121+
return nil, fmt.Errorf("invalid content type %q", wire.Type)
122122
}
123-
return &ResourceContents{
124-
URI: uri,
125-
MIMEType: mimeType,
126-
Blob: blob,
123+
switch wire.Type {
124+
case "text":
125+
v := new(TextContent)
126+
v.fromWire(wire)
127+
return v, nil
128+
case "image":
129+
v := new(ImageContent)
130+
v.fromWire(wire)
131+
return v, nil
132+
case "audio":
133+
v := new(AudioContent)
134+
v.fromWire(wire)
135+
return v, nil
136+
case "resource":
137+
v := new(EmbeddedResource)
138+
v.fromWire(wire)
139+
return v, nil
127140
}
141+
return nil, fmt.Errorf("internal error: unrecognized content type %s", wire.Type)
128142
}

0 commit comments

Comments
 (0)