mcp,design: revert 'content' back to an interface type (#29)
After some experience with the flattened version of Content, we see that it can easily lead to incorrect usage and is a harder API to read. Therefore, this CL changes back to a design similar to what we had prior to https://go.dev/cl/672415, though opting to promote content unmarshalling to the protocol types that use it, which leads to an overall simpler api.
This commit is contained in:
parent
d6ea827820
commit
204b59483b
17 changed files with 333 additions and 159 deletions
|
|
@ -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)
|
||||
|
|
|
|||
218
mcp/content.go
218
mcp/content.go
|
|
@ -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"`
|
||||
// TODO(rfindley): add ResourceLink.
|
||||
type Content interface {
|
||||
MarshalJSON() ([]byte, error)
|
||||
fromWire(*wireContent)
|
||||
}
|
||||
|
||||
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
|
||||
// TextContent is a textual content.
|
||||
type TextContent struct {
|
||||
Text string
|
||||
Meta Meta
|
||||
Annotations *Annotations
|
||||
}
|
||||
|
||||
// NewTextContent creates a [ContentBlock] with text.
|
||||
func NewTextContent(text string) *ContentBlock {
|
||||
return &ContentBlock{Type: "text", Text: text}
|
||||
func (c *TextContent) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&wireContent{
|
||||
Type: "text",
|
||||
Text: c.Text,
|
||||
Meta: c.Meta,
|
||||
Annotations: c.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) fromWire(wire *wireContent) {
|
||||
c.Text = wire.Text
|
||||
c.Meta = wire.Meta
|
||||
c.Annotations = wire.Annotations
|
||||
}
|
||||
|
||||
// NewAudioContent creates a [ContentBlock] with audio data.
|
||||
func NewAudioContent(data []byte, mimeType string) *ContentBlock {
|
||||
return &ContentBlock{Type: "audio", Data: data, MIMEType: mimeType}
|
||||
// ImageContent contains base64-encoded image data.
|
||||
type ImageContent struct {
|
||||
Meta Meta
|
||||
Annotations *Annotations
|
||||
Data []byte // base64-encoded
|
||||
MIMEType string
|
||||
}
|
||||
|
||||
// NewResourceLink creates a [ContentBlock] with a [Resource].
|
||||
func NewResourceLink(r *Resource) *ContentBlock {
|
||||
return &ContentBlock{Type: "resource_link", ResourceLink: r}
|
||||
func (c *ImageContent) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&wireContent{
|
||||
Type: "image",
|
||||
MIMEType: c.MIMEType,
|
||||
Data: c.Data,
|
||||
Meta: c.Meta,
|
||||
Annotations: c.Annotations,
|
||||
})
|
||||
}
|
||||
|
||||
// NewResourceContents creates a [ContentBlock] with an embedded resource (a [ResourceContents]).
|
||||
func NewResourceContents(rc *ResourceContents) *ContentBlock {
|
||||
return &ContentBlock{Type: "resource", Resource: rc}
|
||||
func (c *ImageContent) fromWire(wire *wireContent) {
|
||||
c.MIMEType = wire.MIMEType
|
||||
c.Data = wire.Data
|
||||
c.Meta = wire.Meta
|
||||
c.Annotations = wire.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.
|
||||
// AudioContent contains base64-encoded audio data.
|
||||
type AudioContent struct {
|
||||
Data []byte
|
||||
MIMEType string
|
||||
Meta Meta
|
||||
Annotations *Annotations
|
||||
}
|
||||
|
||||
// A ResourceContents is either a TextResourceContents or a BlobResourceContents.
|
||||
// Use [NewTextResourceContents] or [NextBlobResourceContents] to create one.
|
||||
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
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Blob []byte `json:"blob,omitempty"`
|
||||
Meta Meta `json:"_meta,omitempty"`
|
||||
}
|
||||
|
||||
func (r ResourceContents) MarshalJSON() ([]byte, error) {
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
// 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{}
|
||||
}
|
||||
return &ResourceContents{
|
||||
URI: uri,
|
||||
MIMEType: mimeType,
|
||||
Blob: blob,
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ package mcp_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
|
@ -14,30 +16,73 @@ import (
|
|||
|
||||
func TestContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
in *mcp.ContentBlock
|
||||
in mcp.Content
|
||||
want string // json serialization
|
||||
}{
|
||||
{mcp.NewTextContent("hello"), `{"type":"text","text":"hello"}`},
|
||||
{
|
||||
mcp.NewImageContent([]byte("a1b2c3"), "image/png"),
|
||||
&mcp.TextContent{Text: "hello"},
|
||||
`{"type":"text","text":"hello"}`,
|
||||
},
|
||||
{
|
||||
&mcp.TextContent{
|
||||
Text: "hello",
|
||||
Meta: mcp.Meta{"key": "value"},
|
||||
Annotations: &mcp.Annotations{Priority: 1.0},
|
||||
},
|
||||
`{"type":"text","text":"hello","_meta":{"key":"value"},"annotations":{"priority":1}}`,
|
||||
},
|
||||
{
|
||||
&mcp.ImageContent{
|
||||
Data: []byte("a1b2c3"),
|
||||
MIMEType: "image/png",
|
||||
},
|
||||
`{"type":"image","mimeType":"image/png","data":"YTFiMmMz"}`,
|
||||
},
|
||||
{
|
||||
mcp.NewAudioContent([]byte("a1b2c3"), "audio/wav"),
|
||||
&mcp.ImageContent{
|
||||
Data: []byte("a1b2c3"),
|
||||
MIMEType: "image/png",
|
||||
Meta: mcp.Meta{"key": "value"},
|
||||
Annotations: &mcp.Annotations{Priority: 1.0},
|
||||
},
|
||||
`{"type":"image","mimeType":"image/png","data":"YTFiMmMz","_meta":{"key":"value"},"annotations":{"priority":1}}`,
|
||||
},
|
||||
{
|
||||
&mcp.AudioContent{
|
||||
Data: []byte("a1b2c3"),
|
||||
MIMEType: "audio/wav",
|
||||
},
|
||||
`{"type":"audio","mimeType":"audio/wav","data":"YTFiMmMz"}`,
|
||||
},
|
||||
{
|
||||
mcp.NewResourceContents(
|
||||
mcp.NewTextResourceContents("file://foo", "text", "abc"),
|
||||
),
|
||||
&mcp.AudioContent{
|
||||
Data: []byte("a1b2c3"),
|
||||
MIMEType: "audio/wav",
|
||||
Meta: mcp.Meta{"key": "value"},
|
||||
Annotations: &mcp.Annotations{Priority: 1.0},
|
||||
},
|
||||
`{"type":"audio","mimeType":"audio/wav","data":"YTFiMmMz","_meta":{"key":"value"},"annotations":{"priority":1}}`,
|
||||
},
|
||||
{
|
||||
&mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{URI: "file://foo", MIMEType: "text", Text: "abc"},
|
||||
},
|
||||
`{"type":"resource","resource":{"uri":"file://foo","mimeType":"text","text":"abc"}}`,
|
||||
},
|
||||
{
|
||||
mcp.NewResourceContents(
|
||||
mcp.NewBlobResourceContents("file://foo", "image/png", []byte("a1b2c3")),
|
||||
),
|
||||
&mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{URI: "file://foo", MIMEType: "image/png", Blob: []byte("a1b2c3")},
|
||||
},
|
||||
`{"type":"resource","resource":{"uri":"file://foo","mimeType":"image/png","blob":"YTFiMmMz"}}`,
|
||||
},
|
||||
{
|
||||
&mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{URI: "file://foo", MIMEType: "text", Text: "abc"},
|
||||
Meta: mcp.Meta{"key": "value"},
|
||||
Annotations: &mcp.Annotations{Priority: 1.0},
|
||||
},
|
||||
`{"type":"resource","resource":{"uri":"file://foo","mimeType":"text","text":"abc"},"_meta":{"key":"value"},"annotations":{"priority":1}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
@ -48,39 +93,41 @@ 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.ContentBlock
|
||||
if err := json.Unmarshal(got, &out); err != nil {
|
||||
result := fmt.Sprintf(`{"content":[%s]}`, string(got))
|
||||
log.Println(result)
|
||||
var out mcp.CallToolResult
|
||||
if err := json.Unmarshal([]byte(result), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff := cmp.Diff(test.in, out); diff != "" {
|
||||
if diff := cmp.Diff(test.in, out.Content[0]); diff != "" {
|
||||
t.Errorf("json.Unmarshal(%q) mismatch (-want +got):\n%s", string(got), diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceContents(t *testing.T) {
|
||||
func TestEmbeddedResource(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
rc mcp.ResourceContents
|
||||
rc *mcp.ResourceContents
|
||||
want string // marshaled JSON
|
||||
}{
|
||||
{
|
||||
mcp.ResourceContents{URI: "u", Text: "t"},
|
||||
&mcp.ResourceContents{URI: "u", Text: "t"},
|
||||
`{"uri":"u","text":"t"}`,
|
||||
},
|
||||
{
|
||||
mcp.ResourceContents{URI: "u", MIMEType: "m", Text: "t"},
|
||||
`{"uri":"u","mimeType":"m","text":"t"}`,
|
||||
&mcp.ResourceContents{URI: "u", MIMEType: "m", Text: "t", Meta: mcp.Meta{"key": "value"}},
|
||||
`{"uri":"u","mimeType":"m","text":"t","_meta":{"key":"value"}}`,
|
||||
},
|
||||
{
|
||||
mcp.ResourceContents{URI: "u", Text: "", Blob: nil},
|
||||
`{"uri":"u","text":""}`,
|
||||
&mcp.ResourceContents{URI: "u"},
|
||||
`{"uri":"u"}`,
|
||||
},
|
||||
{
|
||||
mcp.ResourceContents{URI: "u", Blob: []byte{}},
|
||||
&mcp.ResourceContents{URI: "u", Blob: []byte{}},
|
||||
`{"uri":"u","blob":""}`,
|
||||
},
|
||||
{
|
||||
mcp.ResourceContents{URI: "u", Blob: []byte{1}},
|
||||
&mcp.ResourceContents{URI: "u", Blob: []byte{1}},
|
||||
`{"uri":"u","blob":"AQ=="}`,
|
||||
},
|
||||
} {
|
||||
|
|
@ -91,10 +138,11 @@ func TestResourceContents(t *testing.T) {
|
|||
if got := string(data); got != tt.want {
|
||||
t.Errorf("%#v:\ngot %s\nwant %s", tt.rc, got, tt.want)
|
||||
}
|
||||
var urc mcp.ResourceContents
|
||||
if err := json.Unmarshal(data, &urc); err != nil {
|
||||
urc := new(mcp.ResourceContents)
|
||||
if err := json.Unmarshal(data, urc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Since Blob is omitempty, the empty slice changes to nil.
|
||||
if diff := cmp.Diff(tt.rc, urc); diff != "" {
|
||||
t.Errorf("mismatch (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ type SayHiParams struct {
|
|||
|
||||
func SayHi(ctx context.Context, cc *ServerSession, params *CallToolParamsFor[SayHiParams]) (*CallToolResultFor[any], error) {
|
||||
return &CallToolResultFor[any]{
|
||||
Content: []*ContentBlock{
|
||||
NewTextContent("Hi " + params.Name),
|
||||
Content: []Content{
|
||||
&TextContent{Text: "Hi " + params.Name},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: []*ContentBlock{NewTextContent("hi " + params.Arguments.Name)}}, nil
|
||||
return &CallToolResultFor[any]{Content: []Content{&TextContent{Text: "hi " + params.Arguments.Name}}}, nil
|
||||
}
|
||||
|
||||
func TestEndToEnd(t *testing.T) {
|
||||
|
|
@ -142,7 +142,7 @@ func TestEndToEnd(t *testing.T) {
|
|||
wantReview := &GetPromptResult{
|
||||
Description: "Code review prompt",
|
||||
Messages: []*PromptMessage{{
|
||||
Content: NewTextContent("Please review the following code: 1+1"),
|
||||
Content: &TextContent{Text: "Please review the following code: 1+1"},
|
||||
Role: "user",
|
||||
}},
|
||||
}
|
||||
|
|
@ -195,7 +195,9 @@ func TestEndToEnd(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
wantHi := &CallToolResult{
|
||||
Content: []*ContentBlock{{Type: "text", Text: "hi user"}},
|
||||
Content: []Content{
|
||||
&TextContent{Text: "hi user"},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(wantHi, gotHi); diff != "" {
|
||||
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
|
||||
|
|
@ -212,7 +214,9 @@ func TestEndToEnd(t *testing.T) {
|
|||
}
|
||||
wantFail := &CallToolResult{
|
||||
IsError: true,
|
||||
Content: []*ContentBlock{{Type: "text", Text: errTestFailure.Error()}},
|
||||
Content: []Content{
|
||||
&TextContent{Text: errTestFailure.Error()},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(wantFail, gotFail); diff != "" {
|
||||
t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff)
|
||||
|
|
@ -451,7 +455,7 @@ var (
|
|||
return &GetPromptResult{
|
||||
Description: "Code review prompt",
|
||||
Messages: []*PromptMessage{
|
||||
{Role: "user", Content: NewTextContent("Please review the following code: " + params.Arguments["Code"])},
|
||||
{Role: "user", Content: &TextContent{Text: "Please review the following code: " + params.Arguments["Code"]}},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
|
|
@ -505,7 +509,9 @@ func handleEmbeddedResource(_ context.Context, _ *ServerSession, params *ReadRes
|
|||
return nil, fmt.Errorf("no embedded resource named %q", key)
|
||||
}
|
||||
return &ReadResourceResult{
|
||||
Contents: []*ResourceContents{NewTextResourceContents(params.URI, "text/plain", text)},
|
||||
Contents: []*ResourceContents{
|
||||
{URI: params.URI, MIMEType: "text/plain", Text: text},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ package mcp
|
|||
// sdiff -l <(curl $prefix/2025-03-26/schema.ts) <(curl $prefix/2025/06-18/schema.ts)
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/jsonschema"
|
||||
)
|
||||
|
||||
|
|
@ -63,7 +65,7 @@ type CallToolResult struct {
|
|||
Meta `json:"_meta,omitempty"`
|
||||
// A list of content objects that represent the unstructured result of the tool
|
||||
// call.
|
||||
Content []*ContentBlock `json:"content"`
|
||||
Content []Content `json:"content"`
|
||||
// Whether the tool call ended in an error.
|
||||
//
|
||||
// If not set, this is assumed to be false (the call was successful).
|
||||
|
|
@ -83,13 +85,32 @@ type CallToolResult struct {
|
|||
StructuredContent map[string]any `json:"structuredContent,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles the unmarshalling of content into the Content
|
||||
// interface.
|
||||
func (x *CallToolResult) UnmarshalJSON(data []byte) error {
|
||||
type res CallToolResult // avoid recursion
|
||||
var wire struct {
|
||||
res
|
||||
Content []*wireContent `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &wire); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
*x = CallToolResult(wire.res)
|
||||
return nil
|
||||
}
|
||||
|
||||
type CallToolResultFor[Out any] struct {
|
||||
// This property is reserved by the protocol to allow clients and servers to
|
||||
// attach additional metadata to their responses.
|
||||
Meta `json:"_meta,omitempty"`
|
||||
// A list of content objects that represent the unstructured result of the tool
|
||||
// call.
|
||||
Content []*ContentBlock `json:"content"`
|
||||
Content []Content `json:"content"`
|
||||
// Whether the tool call ended in an error.
|
||||
//
|
||||
// If not set, this is assumed to be false (the call was successful).
|
||||
|
|
@ -177,7 +198,7 @@ type CreateMessageResult struct {
|
|||
// This property is reserved by the protocol to allow clients and servers to
|
||||
// attach additional metadata to their responses.
|
||||
Meta `json:"_meta,omitempty"`
|
||||
Content any `json:"content"`
|
||||
Content Content `json:"content"`
|
||||
// The name of the model that generated the message.
|
||||
Model string `json:"model"`
|
||||
Role Role `json:"role"`
|
||||
|
|
@ -518,8 +539,27 @@ func (x *PromptListChangedParams) SetProgressToken(t any) { setProgressToken(x,
|
|||
// This is similar to `SamplingMessage`, but also supports the embedding of
|
||||
// resources from the MCP server.
|
||||
type PromptMessage struct {
|
||||
Content *ContentBlock `json:"content"`
|
||||
Role Role `json:"role"`
|
||||
Content Content `json:"content"`
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles the unmarshalling of content into the Content
|
||||
// interface.
|
||||
func (m *PromptMessage) UnmarshalJSON(data []byte) error {
|
||||
type msg PromptMessage // avoid recursion
|
||||
var wire struct {
|
||||
msg
|
||||
Content *wireContent `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &wire); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
if wire.msg.Content, err = contentFromWire(wire.Content, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = PromptMessage(wire.msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReadResourceParams struct {
|
||||
|
|
@ -650,8 +690,27 @@ type ElicitationCapabilities struct{}
|
|||
|
||||
// Describes a message issued to or received from an LLM API.
|
||||
type SamplingMessage struct {
|
||||
Content any `json:"content"`
|
||||
Role Role `json:"role"`
|
||||
Content Content `json:"content"`
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles the unmarshalling of content into the Content
|
||||
// interface.
|
||||
func (m *SamplingMessage) UnmarshalJSON(data []byte) error {
|
||||
type msg SamplingMessage // avoid recursion
|
||||
var wire struct {
|
||||
msg
|
||||
Content *wireContent `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &wire); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
if wire.msg.Content, err = contentFromWire(wire.Content, map[string]bool{"text": true, "image": true, "audio": true}); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = SamplingMessage(wire.msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetLevelParams struct {
|
||||
|
|
|
|||
|
|
@ -412,7 +412,9 @@ func fileResourceHandler(dir string) ResourceHandler {
|
|||
return nil, err
|
||||
}
|
||||
// TODO(jba): figure out mime type. Omit for now: Server.readResource will fill it in.
|
||||
return &ReadResourceResult{Contents: []*ResourceContents{NewBlobResourceContents(params.URI, "", data)}}, nil
|
||||
return &ReadResourceResult{Contents: []*ResourceContents{
|
||||
&ResourceContents{URI: params.URI, Blob: data},
|
||||
}}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,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.Arguments.Name),
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "Hi " + params.Arguments.Name},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ func ExampleServer() {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(res.Content[0].Text)
|
||||
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
|
||||
|
||||
clientSession.Close()
|
||||
serverSession.Wait()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ type AddParams struct {
|
|||
|
||||
func Add(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[AddParams]) (*mcp.CallToolResultFor[any], error) {
|
||||
return &mcp.CallToolResultFor[any]{
|
||||
Content: []*mcp.ContentBlock{mcp.NewTextContent(fmt.Sprintf("%d", params.Arguments.X+params.Arguments.Y))},
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("%d", params.Arguments.X+params.Arguments.Y)},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +50,7 @@ func ExampleSSEHandler() {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(res.Content[0].Text)
|
||||
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
|
||||
|
||||
// Output: 3
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ func TestSSEServer(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
wantHi := &CallToolResult{
|
||||
Content: []*ContentBlock{{Type: "text", Text: "hi user"}},
|
||||
Content: []Content{
|
||||
&TextContent{Text: "hi user"},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(wantHi, gotHi); diff != "" {
|
||||
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ func TestStreamableTransports(t *testing.T) {
|
|||
|
||||
// 5. Verify that the correct response is received.
|
||||
want := &CallToolResult{
|
||||
Content: []*ContentBlock{{Type: "text", Text: "hi streamy"}},
|
||||
Content: []Content{
|
||||
&TextContent{Text: "hi streamy"},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("CallTool() returned unexpected content (-want +got):\n%s", diff)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ code_review
|
|||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"capabilities": {
|
||||
"completions": {},
|
||||
"logging": {},
|
||||
|
|
@ -49,7 +48,6 @@ code_review
|
|||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"tools": []
|
||||
}
|
||||
}
|
||||
|
|
@ -57,7 +55,6 @@ code_review
|
|||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"prompts": [
|
||||
{
|
||||
"arguments": [
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ info.txt
|
|||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"capabilities": {
|
||||
"completions": {},
|
||||
"logging": {},
|
||||
|
|
@ -69,7 +68,6 @@ info.txt
|
|||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"resources": [
|
||||
{
|
||||
"mimeType": "text/plain",
|
||||
|
|
@ -88,7 +86,6 @@ info.txt
|
|||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"contents": [
|
||||
{
|
||||
"uri": "embedded:info",
|
||||
|
|
@ -107,7 +104,6 @@ info.txt
|
|||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"contents": [
|
||||
{
|
||||
"uri": "file:///info.txt",
|
||||
|
|
|
|||
4
mcp/testdata/conformance/server/tools.txtar
vendored
4
mcp/testdata/conformance/server/tools.txtar
vendored
|
|
@ -27,7 +27,6 @@ greet
|
|||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"capabilities": {
|
||||
"completions": {},
|
||||
"logging": {},
|
||||
|
|
@ -52,7 +51,6 @@ greet
|
|||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"tools": [
|
||||
{
|
||||
"description": "say hi",
|
||||
|
|
@ -79,7 +77,6 @@ greet
|
|||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"resources": []
|
||||
}
|
||||
}
|
||||
|
|
@ -87,7 +84,6 @@ greet
|
|||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"prompts": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ response with its latest supported version.
|
|||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"capabilities": {
|
||||
"completions": {},
|
||||
"logging": {},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ support.
|
|||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"_meta": {},
|
||||
"capabilities": {
|
||||
"completions": {},
|
||||
"logging": {},
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ func newRawHandler(st *ServerTool) rawToolHandler {
|
|||
// rather than returned as jsonrpc2 server errors.
|
||||
if err != nil {
|
||||
return &CallToolResult{
|
||||
Content: []*ContentBlock{NewTextContent(err.Error())},
|
||||
Content: []Content{&TextContent{Text: err.Error()}},
|
||||
IsError: true,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue