Skip to content
Merged
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
22 changes: 9 additions & 13 deletions design/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ These types will be included in the `mcp` package, but will be unexported unless

For user-provided data, we use `json.RawMessage` or `map[string]any`, depending on the use case.

For union types, which can't be represented in Go (specifically `Content` and `ResourceContents`), we prefer distinguished unions: struct types with fields corresponding to the union of all properties for union elements.
For union types, which can't be represented in Go, we use an interface for `Content` (implemented by types like `TextContent`). For other union types like `ResourceContents`, we use a struct with optional fields.

For brevity, only a few examples are shown here:

Expand All @@ -284,20 +284,16 @@ type CallToolResult struct {
IsError bool `json:"isError,omitempty"`
}

// Content is the wire format for content.
//
// The Type field distinguishes the type of the content.
// At most one of Text, MIMEType, Data, and Resource is non-zero.
type Content struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
MIMEType string `json:"mimeType,omitempty"`
Data []byte `json:"data,omitempty"`
Resource *ResourceContents `json:"resource,omitempty"`
// A Content is a [TextContent], [ImageContent], [AudioContent] or
// [EmbeddedResource].
type Content interface {
// (unexported methods)
}

// NewTextContent creates a [Content] with text.
func NewTextContent(text string) *Content
// TextContent is a textual content.
type TextContent struct {
Text string
}
// etc.
```

Expand Down
10 changes: 6 additions & 4 deletions examples/hello/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ type HiArgs struct {

func SayHi(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[HiArgs]) (*mcp.CallToolResultFor[struct{}], error) {
return &mcp.CallToolResultFor[struct{}]{
Content: []*mcp.ContentBlock{
mcp.NewTextContent("Hi " + params.Name),
Content: []mcp.Content{
&mcp.TextContent{Text: "Hi " + params.Name},
},
}, nil
}
Expand All @@ -36,7 +36,7 @@ func PromptHi(ctx context.Context, ss *mcp.ServerSession, params *mcp.GetPromptP
return &mcp.GetPromptResult{
Description: "Code review prompt",
Messages: []*mcp.PromptMessage{
{Role: "user", Content: mcp.NewTextContent("Say hi to " + params.Arguments["name"])},
{Role: "user", Content: &mcp.TextContent{Text: "Say hi to " + params.Arguments["name"]}},
},
}, nil
}
Expand Down Expand Up @@ -93,6 +93,8 @@ func handleEmbeddedResource(_ context.Context, _ *mcp.ServerSession, params *mcp
return nil, fmt.Errorf("no embedded resource named %q", key)
}
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{mcp.NewTextResourceContents(params.URI, "text/plain", text)},
Contents: []*mcp.ResourceContents{
{URI: params.URI, MIMEType: "text/plain", Text: text},
},
}, nil
}
4 changes: 2 additions & 2 deletions examples/sse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type SayHiParams struct {

func SayHi(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[SayHiParams]) (*mcp.CallToolResultFor[any], error) {
return &mcp.CallToolResultFor[any]{
Content: []*mcp.ContentBlock{
mcp.NewTextContent("Hi " + params.Name),
Content: []mcp.Content{
&mcp.TextContent{Text: "Hi " + params.Name},
},
}, nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/readme/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func main() {
log.Fatal("tool failed")
}
for _, c := range res.Content {
log.Print(c.Text)
log.Print(c.(*mcp.TextContent).Text)
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/readme/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type HiParams struct {

func SayHi(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) {
return &mcp.CallToolResultFor[any]{
Content: []*mcp.ContentBlock{mcp.NewTextContent("Hi " + params.Name)},
Content: []mcp.Content{&mcp.TextContent{Text: "Hi " + params.Name}},
}, nil
}

Expand Down
4 changes: 3 additions & 1 deletion mcp/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ func TestCmdTransport(t *testing.T) {
log.Fatal(err)
}
want := &mcp.CallToolResult{
Content: []*mcp.ContentBlock{{Type: "text", Text: "Hi user"}},
Content: []mcp.Content{
&mcp.TextContent{Text: "Hi user"},
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("greet returned unexpected content (-want +got):\n%s", diff)
Expand Down
218 changes: 142 additions & 76 deletions mcp/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,81 +10,117 @@ import (
"fmt"
)

// A ContentBlock is one of a TextContent, ImageContent, AudioContent
// ResourceLink, or EmbeddedResource.
// Use [NewTextContent], [NewImageContent], [NewAudioContent], [NewResourceLink]
// or [NewResourceContents] to create one.
// A Content is a [TextContent], [ImageContent], [AudioContent] or
// [EmbeddedResource].
//
// The Type field must be one of "text", "image", "audio", "resource_link" or "resource".
// The constructors above populate this field appropriately.
// Although at most one of Text, Data, ResourceLink and Resource should be non-zero,
// consumers of ContentBlock use the Type field to determine which value to use;
// values in the other fields are ignored.
// TODO(jba,rfindley): rethink this type. Each kind (text, image, etc.) should have its own
// meta and annotations, otherwise they're duplicated for Resource and ResourceContents.
type ContentBlock struct {
Meta map[string]any `json:"_meta,omitempty"`
Type string `json:"type"`
Text string `json:"text,omitempty"`
MIMEType string `json:"mimeType,omitempty"`
Data []byte `json:"data,omitempty"`
ResourceLink *Resource `json:"resource_link,omitempty"`
Resource *ResourceContents `json:"resource,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
}

func (c *ContentBlock) UnmarshalJSON(data []byte) error {
type wireContent ContentBlock // for naive unmarshaling
var c2 wireContent
if err := json.Unmarshal(data, &c2); err != nil {
return err
}
switch c2.Type {
case "text", "image", "audio", "resource", "resource_link":
default:
return fmt.Errorf("unrecognized content type %s", c.Type)
}
*c = ContentBlock(c2)
return nil
// TODO(rfindley): add ResourceLink.
type Content interface {
MarshalJSON() ([]byte, error)
fromWire(*wireContent)
}

// NewTextContent creates a [ContentBlock] with text.
func NewTextContent(text string) *ContentBlock {
return &ContentBlock{Type: "text", Text: text}
// TextContent is a textual content.
type TextContent struct {
Text string
Meta Meta
Annotations *Annotations
}

// NewImageContent creates a [ContentBlock] with image data.
func NewImageContent(data []byte, mimeType string) *ContentBlock {
return &ContentBlock{Type: "image", Data: data, MIMEType: mimeType}
func (c *TextContent) MarshalJSON() ([]byte, error) {
return json.Marshal(&wireContent{
Type: "text",
Text: c.Text,
Meta: c.Meta,
Annotations: c.Annotations,
})
}

// NewAudioContent creates a [ContentBlock] with audio data.
func NewAudioContent(data []byte, mimeType string) *ContentBlock {
return &ContentBlock{Type: "audio", Data: data, MIMEType: mimeType}
func (c *TextContent) fromWire(wire *wireContent) {
c.Text = wire.Text
c.Meta = wire.Meta
c.Annotations = wire.Annotations
}

// NewResourceLink creates a [ContentBlock] with a [Resource].
func NewResourceLink(r *Resource) *ContentBlock {
return &ContentBlock{Type: "resource_link", ResourceLink: r}
// ImageContent contains base64-encoded image data.
type ImageContent struct {
Meta Meta
Annotations *Annotations
Data []byte // base64-encoded
MIMEType string
}

// NewResourceContents creates a [ContentBlock] with an embedded resource (a [ResourceContents]).
func NewResourceContents(rc *ResourceContents) *ContentBlock {
return &ContentBlock{Type: "resource", Resource: rc}
func (c *ImageContent) MarshalJSON() ([]byte, error) {
return json.Marshal(&wireContent{
Type: "image",
MIMEType: c.MIMEType,
Data: c.Data,
Meta: c.Meta,
Annotations: c.Annotations,
})
}

// ResourceContents represents the union of the spec's {Text,Blob}ResourceContents types.
// See https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts#L524-L551
// for the inheritance structure.
func (c *ImageContent) fromWire(wire *wireContent) {
c.MIMEType = wire.MIMEType
c.Data = wire.Data
c.Meta = wire.Meta
c.Annotations = wire.Annotations
}

// AudioContent contains base64-encoded audio data.
type AudioContent struct {
Data []byte
MIMEType string
Meta Meta
Annotations *Annotations
}

func (c AudioContent) MarshalJSON() ([]byte, error) {
return json.Marshal(&wireContent{
Type: "audio",
MIMEType: c.MIMEType,
Data: c.Data,
Meta: c.Meta,
Annotations: c.Annotations,
})
}

func (c *AudioContent) fromWire(wire *wireContent) {
c.MIMEType = wire.MIMEType
c.Data = wire.Data
c.Meta = wire.Meta
c.Annotations = wire.Annotations
}

// A ResourceContents is either a TextResourceContents or a BlobResourceContents.
// Use [NewTextResourceContents] or [NextBlobResourceContents] to create one.
// EmbeddedResource contains embedded resources.
type EmbeddedResource struct {
Resource *ResourceContents
Meta Meta
Annotations *Annotations
}

func (c *EmbeddedResource) MarshalJSON() ([]byte, error) {
return json.Marshal(&wireContent{
Type: "resource",
Resource: c.Resource,
Meta: c.Meta,
Annotations: c.Annotations,
})
}

func (c *EmbeddedResource) fromWire(wire *wireContent) {
c.Resource = wire.Resource
c.Meta = wire.Meta
c.Annotations = wire.Annotations
}

// ResourceContents contains the contents of a specific resource or
// sub-resource.
type ResourceContents struct {
Meta map[string]any `json:"_meta,omitempty"`
URI string `json:"uri"` // resource location; must not be empty
MIMEType string `json:"mimeType,omitempty"`
Text string `json:"text"`
Blob []byte `json:"blob,omitempty"` // if nil, then text; else blob
URI string `json:"uri"`
MIMEType string `json:"mimeType,omitempty"`
Text string `json:"text,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

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

We had talked about using the same interface-plus-concrete-types pattern here, for consistency. Why aren't we?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did that, and it added a lot of code and complexity for little value. Unlike contents, this isn't a distinguished union: the presence of text or blob determines its type, and otherwise the types are the same. We can make it an interface, but maybe in a later CL?

Blob []byte `json:"blob,omitempty"`
Meta Meta `json:"_meta,omitempty"`
}

func (r ResourceContents) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -114,25 +150,55 @@ func (r ResourceContents) MarshalJSON() ([]byte, error) {
return json.Marshal(br)
}

// NewTextResourceContents returns a [ResourceContents] containing text.
func NewTextResourceContents(uri, mimeType, text string) *ResourceContents {
return &ResourceContents{
URI: uri,
MIMEType: mimeType,
Text: text,
// Blob is nil, indicating this is a TextResourceContents.
// wireContent is the wire format for content.
// It represents the protocol types TextContent, ImageContent, AudioContent
// and EmbeddedResource.
// The Type field distinguishes them. In the protocol, each type has a constant
// value for the field.
// At most one of Text, Data, and Resource is non-zero.
type wireContent struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
MIMEType string `json:"mimeType,omitempty"`
Data []byte `json:"data,omitempty"`
Resource *ResourceContents `json:"resource,omitempty"`
Meta Meta `json:"_meta,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
}

func contentsFromWire(wires []*wireContent, allow map[string]bool) ([]Content, error) {
var blocks []Content
for _, wire := range wires {
block, err := contentFromWire(wire, allow)
if err != nil {
return nil, err
}
blocks = append(blocks, block)
}
return blocks, nil
}

// NewBlobResourceContents returns a [ResourceContents] containing a byte slice.
func NewBlobResourceContents(uri, mimeType string, blob []byte) *ResourceContents {
// The only way to distinguish text from blob is a non-nil Blob field.
if blob == nil {
blob = []byte{}
func contentFromWire(wire *wireContent, allow map[string]bool) (Content, error) {
if allow != nil && !allow[wire.Type] {
return nil, fmt.Errorf("invalid content type %q", wire.Type)
}
return &ResourceContents{
URI: uri,
MIMEType: mimeType,
Blob: blob,
switch wire.Type {
case "text":
v := new(TextContent)
v.fromWire(wire)
return v, nil
case "image":
v := new(ImageContent)
v.fromWire(wire)
return v, nil
case "audio":
v := new(AudioContent)
v.fromWire(wire)
return v, nil
case "resource":
v := new(EmbeddedResource)
v.fromWire(wire)
return v, nil
}
return nil, fmt.Errorf("internal error: unrecognized content type %s", wire.Type)
}
Loading
Loading