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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,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.Content{mcp.NewTextContent("Hi " + params.Name)},
Content: []*mcp.ContentBlock{mcp.NewTextContent("Hi " + params.Name)},
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion examples/hello/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ 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.Content{
Content: []*mcp.ContentBlock{
mcp.NewTextContent("Hi " + params.Name),
},
}, nil
Expand Down
2 changes: 1 addition & 1 deletion examples/sse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ 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.Content{
Content: []*mcp.ContentBlock{
mcp.NewTextContent("Hi " + params.Name),
},
}, nil
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.Content{mcp.NewTextContent("Hi " + params.Name)},
Content: []*mcp.ContentBlock{mcp.NewTextContent("Hi " + params.Name)},
}, nil
}

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

// Content is the wire format for content.
// It represents the protocol types TextContent, ImageContent, AudioContent
// and EmbeddedResource.
// Use [NewTextContent], [NewImageContent], [NewAudioContent] or [NewResourceContent]
// to create one.
// A ContentBlock is one of a TextContent, ImageContent, AudioContent
// ResourceLink, or EmbeddedResource.
// Use [NewTextContent], [NewImageContent], [NewAudioContent], [NewResourceLink]
// or [NewResourceContents] to create one.
//
// The Type field must be one of "text", "image", "audio" or "resource". The
// constructors above populate this field appropriately.
// Although at most one of Text, Data, and Resource should be non-zero, consumers of Content
// use the Type field to determine which value to use; values in the other fields are ignored.
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"`
Annotations *Annotations `json:"annotations,omitempty"`
// 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 *Content) UnmarshalJSON(data []byte) error {
type wireContent Content // for naive unmarshaling
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":
case "text", "image", "audio", "resource", "resource_link":
default:
return fmt.Errorf("unrecognized content type %s", c.Type)
}
*c = Content(c2)
*c = ContentBlock(c2)
return nil
}

// NewTextContent creates a [Content] with text.
func NewTextContent(text string) *Content {
return &Content{Type: "text", Text: text}
// NewTextContent creates a [ContentBlock] with text.
func NewTextContent(text string) *ContentBlock {
return &ContentBlock{Type: "text", Text: text}
}

// NewImageContent creates a [Content] with image data.
func NewImageContent(data []byte, mimeType string) *Content {
return &Content{Type: "image", Data: data, MIMEType: mimeType}
// NewImageContent creates a [ContentBlock] with image data.
func NewImageContent(data []byte, mimeType string) *ContentBlock {
return &ContentBlock{Type: "image", Data: data, MIMEType: mimeType}
}

// NewAudioContent creates a [Content] with audio data.
func NewAudioContent(data []byte, mimeType string) *Content {
return &Content{Type: "audio", Data: data, MIMEType: mimeType}
// NewAudioContent creates a [ContentBlock] with audio data.
func NewAudioContent(data []byte, mimeType string) *ContentBlock {
return &ContentBlock{Type: "audio", Data: data, MIMEType: mimeType}
}

// NewResourceContent creates a [Content] with an embedded resource.
func NewResourceContent(resource *ResourceContents) *Content {
return &Content{Type: "resource", Resource: resource}
// NewResourceLink creates a [ContentBlock] with a [Resource].
func NewResourceLink(r *Resource) *ContentBlock {
return &ContentBlock{Type: "resource_link", ResourceLink: r}
}

// NewResourceContents creates a [ContentBlock] with an embedded resource (a [ResourceContents]).
func NewResourceContents(rc *ResourceContents) *ContentBlock {
return &ContentBlock{Type: "resource", Resource: rc}
}

// ResourceContents represents the union of the spec's {Text,Blob}ResourceContents types.
Expand All @@ -71,10 +80,11 @@ func NewResourceContent(resource *ResourceContents) *Content {
// A ResourceContents is either a TextResourceContents or a BlobResourceContents.
// Use [NewTextResourceContents] or [NextBlobResourceContents] to create one.
type ResourceContents struct {
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
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
}

func (r ResourceContents) MarshalJSON() ([]byte, error) {
Expand Down
8 changes: 4 additions & 4 deletions mcp/content_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

func TestContent(t *testing.T) {
tests := []struct {
in *mcp.Content
in *mcp.ContentBlock
want string // json serialization
}{
{mcp.NewTextContent("hello"), `{"type":"text","text":"hello"}`},
Expand All @@ -27,13 +27,13 @@ func TestContent(t *testing.T) {
`{"type":"audio","mimeType":"audio/wav","data":"YTFiMmMz"}`,
},
{
mcp.NewResourceContent(
mcp.NewResourceContents(
mcp.NewTextResourceContents("file://foo", "text", "abc"),
),
`{"type":"resource","resource":{"uri":"file://foo","mimeType":"text","text":"abc"}}`,
},
{
mcp.NewResourceContent(
mcp.NewResourceContents(
mcp.NewBlobResourceContents("file://foo", "image/png", []byte("a1b2c3")),
),
`{"type":"resource","resource":{"uri":"file://foo","mimeType":"image/png","blob":"YTFiMmMz"}}`,
Expand All @@ -48,7 +48,7 @@ func TestContent(t *testing.T) {
if diff := cmp.Diff(test.want, string(got)); diff != "" {
t.Errorf("json.Marshal(%v) mismatch (-want +got):\n%s", test.in, diff)
}
var out *mcp.Content
var out *mcp.ContentBlock
if err := json.Unmarshal(got, &out); err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 3 additions & 1 deletion mcp/example_progress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ func Example_progressMiddleware() {

func addProgressToken[S mcp.Session](h mcp.MethodHandler[S]) mcp.MethodHandler[S] {
return func(ctx context.Context, s S, method string, params mcp.Params) (result mcp.Result, err error) {
params.GetMeta().ProgressToken = nextProgressToken.Add(1)
if rp, ok := params.(mcp.RequestParams); ok {
rp.SetProgressToken(nextProgressToken.Add(1))
}
return h(ctx, s, method, params)
}
}
2 changes: 1 addition & 1 deletion mcp/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type SayHiParams struct {

func SayHi(ctx context.Context, cc *ServerSession, params *CallToolParamsFor[SayHiParams]) (*CallToolResultFor[any], error) {
return &CallToolResultFor[any]{
Content: []*Content{
Content: []*ContentBlock{
NewTextContent("Hi " + params.Name),
},
}, nil
Expand Down
6 changes: 3 additions & 3 deletions mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func sayHi(ctx context.Context, ss *ServerSession, params *CallToolParamsFor[hiP
if err := ss.Ping(ctx, nil); err != nil {
return nil, fmt.Errorf("ping failed: %v", err)
}
return &CallToolResultFor[any]{Content: []*Content{NewTextContent("hi " + params.Arguments.Name)}}, nil
return &CallToolResultFor[any]{Content: []*ContentBlock{NewTextContent("hi " + params.Arguments.Name)}}, nil
}

func TestEndToEnd(t *testing.T) {
Expand Down Expand Up @@ -195,7 +195,7 @@ func TestEndToEnd(t *testing.T) {
t.Fatal(err)
}
wantHi := &CallToolResult{
Content: []*Content{{Type: "text", Text: "hi user"}},
Content: []*ContentBlock{{Type: "text", Text: "hi user"}},
}
if diff := cmp.Diff(wantHi, gotHi); diff != "" {
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
Expand All @@ -212,7 +212,7 @@ func TestEndToEnd(t *testing.T) {
}
wantFail := &CallToolResult{
IsError: true,
Content: []*Content{{Type: "text", Text: errTestFailure.Error()}},
Content: []*ContentBlock{{Type: "text", Text: errTestFailure.Error()}},
}
if diff := cmp.Diff(wantFail, gotFail); diff != "" {
t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff)
Expand Down
Loading
Loading