internal/mcp: simplify content

Now that the protocol package is merged into the mcp package, clean up
the higher level 'Content' union types, in favor of types constructors.

Change-Id: I37bedb0ff0a296eef799ba65c37ca7f94ee1dbbe
Reviewed-on: https://go-review.googlesource.com/c/tools/+/672415
Reviewed-by: Jonathan Amsterdam <jba@google.com>
TryBot-Bypass: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
Rob Findley 2025-05-13 21:51:26 +00:00 • committed by Jonathan Amsterdam
commit dac5be2c50
15 changed files with 107 additions and 207 deletions

View file

@ -123,6 +123,7 @@ type Stream interface {
Close() error Close() error
} }
``` ```
Methods accept a Go `Context` and return an `error`, Methods accept a Go `Context` and return an `error`,
as is idiomatic for APIs that do I/O. as is idiomatic for APIs that do I/O.
@ -350,17 +351,6 @@ type Content struct {
Data []byte `json:"data,omitempty"` Data []byte `json:"data,omitempty"`
Resource *Resource `json:"resource,omitempty"` Resource *Resource `json:"resource,omitempty"`
} }
// Resource is the wire format for embedded resources.
//
// The URI field describes the resource location. At most one of Text and Blob
// is non-zero.
type Resource struct {
URI string `json:"uri,"`
MIMEType string `json:"mimeType,omitempty"`
Text string `json:"text"`
Blob []byte `json:"blob"`
}
``` ```
**Differences from mcp-go**: these types are largely similar, but our type **Differences from mcp-go**: these types are largely similar, but our type
@ -445,6 +435,7 @@ content, err := session.CallTool(ctx, &CallToolParams{
... ...
return session.Close() return session.Close()
``` ```
A server that can handle that client call would look like this: A server that can handle that client call would look like this:
```go ```go
@ -465,8 +456,6 @@ session until the client disconnects:
func (*Server) Run(context.Context, Transport) func (*Server) Run(context.Context, Transport)
``` ```
**Differences from mcp-go**: the Server APIs are very similar to mcp-go, **Differences from mcp-go**: the Server APIs are very similar to mcp-go,
though the association between servers and transports is different. In though the association between servers and transports is different. In
mcp-go, a single server is bound to what we would call an `SSEHTTPHandler`, mcp-go, a single server is bound to what we would call an `SSEHTTPHandler`,
@ -493,9 +482,11 @@ documentation.
As we saw above, the `ClientSession` method for the specification's As we saw above, the `ClientSession` method for the specification's
`CallTool` RPC takes a context and a params pointer as arguments, and returns a `CallTool` RPC takes a context and a params pointer as arguments, and returns a
result pointer and error: result pointer and error:
```go ```go
func (*ClientSession) CallTool(context.Context, *CallToolParams) (*CallToolResult, error) func (*ClientSession) CallTool(context.Context, *CallToolParams) (*CallToolResult, error)
``` ```
Our SDK has a method for every RPC in the spec, and their signatures all share Our SDK has a method for every RPC in the spec, and their signatures all share
this form. To avoid boilerplate, we don't repeat this signature for RPCs this form. To avoid boilerplate, we don't repeat this signature for RPCs
defined in the spec; readers may assume it when we mention a "spec method." defined in the spec; readers may assume it when we mention a "spec method."
@ -867,12 +858,14 @@ handler to a Go function using reflection to derive its arguments. We provide
To add a resource or resource template to a server, users call the `AddResource` and To add a resource or resource template to a server, users call the `AddResource` and
`AddResourceTemplate` methods, passing the resource or template and a function for reading it: `AddResourceTemplate` methods, passing the resource or template and a function for reading it:
```go ```go
type ReadResourceHandler func(context.Context, *ServerSession, *Resource, *ReadResourceParams) (*ReadResourceResult, error) type ReadResourceHandler func(context.Context, *ServerSession, *Resource, *ReadResourceParams) (*ReadResourceResult, error)
func (*Server) AddResource(*Resource, ReadResourceHandler) func (*Server) AddResource(*Resource, ReadResourceHandler)
func (*Server) AddResourceTemplate(*ResourceTemplate, ReadResourceHandler) func (*Server) AddResourceTemplate(*ResourceTemplate, ReadResourceHandler)
``` ```
The `Resource` is passed to the reader function even though it is redundant (the function could have closed over it) The `Resource` is passed to the reader function even though it is redundant (the function could have closed over it)
so a single handler can support multiple resources. so a single handler can support multiple resources.
If the incoming resource matches a template, a `Resource` argument is constructed If the incoming resource matches a template, a `Resource` argument is constructed
@ -880,15 +873,18 @@ from the fields in the `ResourceTemplate`.
The `ServerSession` argument is there so the reader can observe the client's roots. The `ServerSession` argument is there so the reader can observe the client's roots.
To read files from the local filesystem, we recommend using `FileReadResourceHandler` to construct a handler: To read files from the local filesystem, we recommend using `FileReadResourceHandler` to construct a handler:
```go ```go
// FileReadResourceHandler returns a ReadResourceHandler that reads paths using dir as a root directory. // FileReadResourceHandler returns a ReadResourceHandler that reads paths using dir as a root directory.
// It protects against path traversal attacks. // It protects against path traversal attacks.
// It will not read any file that is not in the root set of the client requesting the resource. // It will not read any file that is not in the root set of the client requesting the resource.
func (*Server) FileReadResourceHandler(dir string) ReadResourceHandler func (*Server) FileReadResourceHandler(dir string) ReadResourceHandler
``` ```
It guards against [path traversal attacks](https://go.dev/blog/osroot) It guards against [path traversal attacks](https://go.dev/blog/osroot)
and observes the client's roots. and observes the client's roots.
Here is an example: Here is an example:
```go ```go
// Safely read "/public/puppies.txt". // Safely read "/public/puppies.txt".
s.AddResource( s.AddResource(
@ -897,14 +893,17 @@ s.AddResource(
``` ```
There are also server methods to remove resources and resource templates. There are also server methods to remove resources and resource templates.
```go ```go
func (*Server) RemoveResources(uris ...string) func (*Server) RemoveResources(uris ...string)
func (*Server) RemoveResourceTemplates(names ...string) func (*Server) RemoveResourceTemplates(names ...string)
``` ```
Resource templates don't have unique identifiers, so removing a name will remove all Resource templates don't have unique identifiers, so removing a name will remove all
resource templates with that name. resource templates with that name.
Servers support all of the resource-related spec methods: Servers support all of the resource-related spec methods:
- `ListResources` and `ListResourceTemplates` for listings. - `ListResources` and `ListResourceTemplates` for listings.
- `ReadResource` to get the contents of a resource. - `ReadResource` to get the contents of a resource.
- `Subscribe` and `Unsubscribe` to manage subscriptions on resources. - `Subscribe` and `Unsubscribe` to manage subscriptions on resources.
@ -916,6 +915,7 @@ then returns the result of calling the associated reader function.
#### Subscriptions #### Subscriptions
ClientSessions can manage change notifications on particular resources: ClientSessions can manage change notifications on particular resources:
```go ```go
func (*ClientSession) Subscribe(context.Context, *SubscribeParams) error func (*ClientSession) Subscribe(context.Context, *SubscribeParams) error
func (*ClientSession) Unsubscribe(context.Context, *UnsubscribeParams) error func (*ClientSession) Unsubscribe(context.Context, *UnsubscribeParams) error
@ -929,6 +929,7 @@ user doesn't have to.
If a server author wants to support resource subscriptions, they must provide handlers If a server author wants to support resource subscriptions, they must provide handlers
to be called when clients subscribe and unsubscribe. It is an error to provide only to be called when clients subscribe and unsubscribe. It is an error to provide only
one of these handlers. one of these handlers.
```go ```go
type ServerOptions struct { type ServerOptions struct {
... ...
@ -940,9 +941,11 @@ type ServerOptions struct {
``` ```
User code should call `ResourceUpdated` when a subscribed resource changes. User code should call `ResourceUpdated` when a subscribed resource changes.
```go ```go
func (*Server) ResourceUpdated(context.Context, *ResourceUpdatedNotification) error func (*Server) ResourceUpdated(context.Context, *ResourceUpdatedNotification) error
``` ```
The server routes these notifications to the server sessions that subscribed to the resource. The server routes these notifications to the server sessions that subscribed to the resource.
### ListChanged notifications ### ListChanged notifications
@ -1007,6 +1010,7 @@ follows:
level, a handler would call `session.Log(ctx, mcp.LevelNotice, "message")`. level, a handler would call `session.Log(ctx, mcp.LevelNotice, "message")`.
A client that wishes to receive log messages must provide a handler: A client that wishes to receive log messages must provide a handler:
```go ```go
type ClientOptions struct { type ClientOptions struct {
... ...

View file

@ -22,7 +22,7 @@ type HiParams struct {
func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *HiParams) ([]mcp.Content, error) { func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *HiParams) ([]mcp.Content, error) {
return []mcp.Content{ return []mcp.Content{
mcp.TextContent{Text: "Hi " + params.Name}, mcp.NewTextContent("Hi " + params.Name),
}, nil }, nil
} }
@ -30,7 +30,7 @@ func PromptHi(ctx context.Context, cc *mcp.ServerConnection, params *HiParams) (
return &mcp.GetPromptResult{ return &mcp.GetPromptResult{
Description: "Code review prompt", Description: "Code review prompt",
Messages: []mcp.PromptMessage{ Messages: []mcp.PromptMessage{
{Role: "user", Content: mcp.TextContent{Text: "Say hi to " + params.Name}.ToWire()}, {Role: "user", Content: mcp.NewTextContent("Say hi to " + params.Name)},
}, },
}, nil }, nil
} }

View file

@ -21,7 +21,7 @@ type SayHiParams struct {
func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *SayHiParams) ([]mcp.Content, error) { func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *SayHiParams) ([]mcp.Content, error) {
return []mcp.Content{ return []mcp.Content{
mcp.TextContent{Text: "Hi " + params.Name}, mcp.NewTextContent("Hi " + params.Name),
}, nil }, nil
} }

View file

@ -10,15 +10,6 @@ import (
"slices" "slices"
) )
// Apply returns a new slice resulting from applying f to each element of x.
func Apply[S ~[]E, E, F any](x S, f func(E) F) []F {
y := make([]F, len(x))
for i, e := range x {
y[i] = f(e)
}
return y
}
// Helpers below are copied from gopls' moremaps package. // Helpers below are copied from gopls' moremaps package.
// sorted returns an iterator over the entries of m in key order. // sorted returns an iterator over the entries of m in key order.

View file

@ -57,7 +57,7 @@ func TestCmdTransport(t *testing.T) {
log.Fatal(err) log.Fatal(err)
} }
want := &mcp.CallToolResult{ want := &mcp.CallToolResult{
Content: []mcp.WireContent{{Type: "text", Text: "Hi user"}}, Content: []mcp.Content{{Type: "text", Text: "Hi user"}},
} }
if diff := cmp.Diff(want, got); diff != "" { if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("greet returned unexpected content (-want +got):\n%s", diff) t.Errorf("greet returned unexpected content (-want +got):\n%s", diff)

View file

@ -11,37 +11,25 @@ import (
// The []byte fields below are marked omitzero, not omitempty: // The []byte fields below are marked omitzero, not omitempty:
// we want to marshal an empty byte slice. // we want to marshal an empty byte slice.
// TODO(jba): figure out how to fix this for 1.23.
// WireContent is the wire format for content. // Content is the wire format for content.
// It represents the protocol types TextContent, ImageContent, AudioContent // It represents the protocol types TextContent, ImageContent, AudioContent
// and EmbeddedResource. // and EmbeddedResource.
// The Type field distinguishes them. In the protocol, each type has a constant // The Type field distinguishes them. In the protocol, each type has a constant
// value for the field. // value for the field.
// At most one of Text, Data, and Resource is non-zero. // At most one of Text, Data, and Resource is non-zero.
type WireContent struct { type Content struct {
Type string `json:"type"` Type string `json:"type"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
MIMEType string `json:"mimeType,omitempty"` MIMEType string `json:"mimeType,omitempty"`
Data []byte `json:"data,omitzero"` Data []byte `json:"data,omitzero"`
Resource *WireResource `json:"resource,omitempty"` Resource *ResourceContents `json:"resource,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"` Annotations *Annotations `json:"annotations,omitempty"`
} }
// A WireResource is either a TextResourceContents or a BlobResourceContents. func (c *Content) UnmarshalJSON(data []byte) error {
// See https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts#L524-L551 type wireContent Content // for naive unmarshaling
// for the inheritance structure.
// If Blob is nil, this is a TextResourceContents; otherwise it's a BlobResourceContents.
//
// The URI field describes the resource location.
type WireResource struct {
URI string `json:"uri,"`
MIMEType string `json:"mimeType,omitempty"`
Text string `json:"text"`
Blob []byte `json:"blob,omitzero"`
}
func (c *WireContent) UnmarshalJSON(data []byte) error {
type wireContent WireContent // for naive unmarshaling
var c2 wireContent var c2 wireContent
if err := json.Unmarshal(data, &c2); err != nil { if err := json.Unmarshal(data, &c2); err != nil {
return err return err
@ -51,77 +39,44 @@ func (c *WireContent) UnmarshalJSON(data []byte) error {
default: default:
return fmt.Errorf("unrecognized content type %s", c.Type) return fmt.Errorf("unrecognized content type %s", c.Type)
} }
*c = WireContent(c2) *c = Content(c2)
return nil return nil
} }
// Content is the union of supported content types: [TextContent], func NewTextContent(text string) Content {
// [ImageContent], [AudioContent], and [ResourceContent]. return Content{Type: "text", Text: text}
}
func NewImageContent(data []byte, mimeType string) Content {
return Content{Type: "image", Data: data, MIMEType: mimeType}
}
func NewAudioContent(data []byte, mimeType string) Content {
return Content{Type: "audio", Data: data, MIMEType: mimeType}
}
func NewResourceContent(resource ResourceContents) Content {
return Content{Type: "resource", Resource: &resource}
}
// A ResourceContents is either a TextResourceContents or a BlobResourceContents.
// See https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts#L524-L551
// for the inheritance structure.
// If Blob is nil, this is a TextResourceContents; otherwise it's a BlobResourceContents.
// //
// ToWire converts content to its jsonrpc2 wire format. // The URI field describes the resource location.
type Content interface { type ResourceContents struct {
// TODO: unexport this, and move the tests that use it to this package. URI string `json:"uri,"`
ToWire() WireContent MIMEType string `json:"mimeType,omitempty"`
Text string `json:"text,omitempty"`
Blob []byte `json:"blob,omitzero"`
} }
// TextContent is a textual content. func NewTextResourceContents(uri, mimeType, text string) ResourceContents {
type TextContent struct { return ResourceContents{
Text string URI: uri,
} MIMEType: mimeType,
Text: text,
func (c TextContent) ToWire() WireContent {
return WireContent{Type: "text", Text: c.Text}
}
// ImageContent contains base64-encoded image data.
type ImageContent struct {
Data []byte // base64-encoded
MIMEType string
}
func (c ImageContent) ToWire() WireContent {
return WireContent{Type: "image", MIMEType: c.MIMEType, Data: c.Data}
}
// AudioContent contains base64-encoded audio data.
type AudioContent struct {
Data []byte
MIMEType string
}
func (c AudioContent) ToWire() WireContent {
return WireContent{Type: "audio", MIMEType: c.MIMEType, Data: c.Data}
}
// ResourceContent contains embedded resources.
type ResourceContent struct {
Resource EmbeddedResource
}
func (r ResourceContent) ToWire() WireContent {
res := r.Resource.toWire()
return WireContent{Type: "resource", Resource: &res}
}
type EmbeddedResource interface {
toWire() WireResource
}
// The {Text,Blob}ResourceContents types match the protocol definitions,
// but we represent both as a single type on the wire.
// A TextResourceContents is the contents of a text resource.
type TextResourceContents struct {
URI string
MIMEType string
Text string
}
func (r TextResourceContents) toWire() WireResource {
return WireResource{
URI: r.URI,
MIMEType: r.MIMEType,
Text: r.Text,
// Blob is nil, indicating this is a TextResourceContents. // Blob is nil, indicating this is a TextResourceContents.
} }
} }
@ -133,43 +88,10 @@ type BlobResourceContents struct {
Blob []byte Blob []byte
} }
func (r BlobResourceContents) toWire() WireResource { func NewBlobResourceContents(uri, mimeType string, blob []byte) ResourceContents {
return WireResource{ return ResourceContents{
URI: r.URI, URI: uri,
MIMEType: r.MIMEType, MIMEType: mimeType,
Blob: r.Blob, Blob: blob,
}
}
// ContentFromWireContent converts content from the jsonrpc2 wire format to a
// typed Content value.
func ContentFromWireContent(c WireContent) Content {
switch c.Type {
case "text":
return TextContent{Text: c.Text}
case "image":
return ImageContent{Data: c.Data, MIMEType: c.MIMEType}
case "audio":
return AudioContent{Data: c.Data, MIMEType: c.MIMEType}
case "resource":
r := ResourceContent{}
if c.Resource != nil {
if c.Resource.Blob != nil {
r.Resource = BlobResourceContents{
URI: c.Resource.URI,
MIMEType: c.Resource.MIMEType,
Blob: c.Resource.Blob,
}
} else {
r.Resource = TextResourceContents{
URI: c.Resource.URI,
MIMEType: c.Resource.MIMEType,
Text: c.Resource.Text,
}
}
}
return r
default:
panic(fmt.Sprintf("unrecognized wire content type %q", c.Type))
} }
} }

View file

@ -5,6 +5,7 @@
package mcp_test package mcp_test
import ( import (
"encoding/json"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@ -14,57 +15,45 @@ import (
func TestContent(t *testing.T) { func TestContent(t *testing.T) {
tests := []struct { tests := []struct {
in mcp.Content in mcp.Content
want mcp.WireContent want string // json serialization
}{ }{
{mcp.TextContent{Text: "hello"}, mcp.WireContent{Type: "text", Text: "hello"}}, {mcp.NewTextContent("hello"), `{"type":"text","text":"hello"}`},
{ {
mcp.ImageContent{Data: []byte("a1b2c3"), MIMEType: "image/png"}, mcp.NewImageContent([]byte("a1b2c3"), "image/png"),
mcp.WireContent{Type: "image", Data: []byte("a1b2c3"), MIMEType: "image/png"}, `{"type":"image","mimeType":"image/png","data":"YTFiMmMz"}`,
}, },
{ {
mcp.AudioContent{Data: []byte("a1b2c3"), MIMEType: "audio/wav"}, mcp.NewAudioContent([]byte("a1b2c3"), "audio/wav"),
mcp.WireContent{Type: "audio", Data: []byte("a1b2c3"), MIMEType: "audio/wav"}, `{"type":"audio","mimeType":"audio/wav","data":"YTFiMmMz"}`,
}, },
{ {
mcp.ResourceContent{ mcp.NewResourceContent(
Resource: mcp.TextResourceContents{ mcp.NewTextResourceContents("file://foo", "text", "abc"),
URI: "file://foo", ),
MIMEType: "text", `{"type":"resource","resource":{"uri":"file://foo","mimeType":"text","text":"abc"}}`,
Text: "abc",
},
},
mcp.WireContent{
Type: "resource",
Resource: &mcp.WireResource{
URI: "file://foo",
MIMEType: "text",
Text: "abc",
},
},
}, },
{ {
mcp.ResourceContent{ mcp.NewResourceContent(
Resource: mcp.BlobResourceContents{ mcp.NewBlobResourceContents("file://foo", "image/png", []byte("a1b2c3")),
URI: "file://foo", ),
MIMEType: "text", `{"type":"resource","resource":{"uri":"file://foo","mimeType":"image/png","blob":"YTFiMmMz"}}`,
Blob: []byte("a1b2c3"),
},
},
mcp.WireContent{
Type: "resource",
Resource: &mcp.WireResource{
URI: "file://foo",
MIMEType: "text",
Blob: []byte("a1b2c3"),
},
},
}, },
} }
for _, test := range tests { for _, test := range tests {
got := test.in.ToWire() got, err := json.Marshal(test.in)
if diff := cmp.Diff(test.want, got); diff != "" { if err != nil {
t.Errorf("ToWire mismatch (-want +got):\n%s", diff) t.Fatal(err)
}
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
if err := json.Unmarshal(got, &out); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(test.in, out); diff != "" {
t.Errorf("json.Unmarshal(%q) mismatch (-want +got):\n%s", string(got), diff)
} }
} }
} }

View file

@ -107,7 +107,7 @@ var declarations = config{
Fields: config{"Params": {Name: "ReadResourceParams"}}, Fields: config{"Params": {Name: "ReadResourceParams"}},
}, },
"ReadResourceResult": { "ReadResourceResult": {
Fields: config{"Contents": {Substitute: "*WireResource"}}, Fields: config{"Contents": {Substitute: "*ResourceContents"}},
}, },
"Resource": {}, "Resource": {},
"Role": {}, "Role": {},
@ -288,7 +288,7 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
if slices.ContainsFunc(def.AnyOf, func(s *jsonschema.Schema) bool { if slices.ContainsFunc(def.AnyOf, func(s *jsonschema.Schema) bool {
return s.Ref == "#/definitions/TextContent" return s.Ref == "#/definitions/TextContent"
}) { }) {
fmt.Fprintf(w, "WireContent") fmt.Fprintf(w, "Content")
} else { } else {
// E.g. union types. // E.g. union types.
fmt.Fprintf(w, "json.RawMessage") fmt.Fprintf(w, "json.RawMessage")

View file

@ -28,7 +28,7 @@ func sayHi(ctx context.Context, cc *ServerConnection, v hiParams) ([]Content, er
if err := cc.Ping(ctx); err != nil { if err := cc.Ping(ctx); err != nil {
return nil, fmt.Errorf("ping failed: %v", err) return nil, fmt.Errorf("ping failed: %v", err)
} }
return []Content{TextContent{Text: "hi " + v.Name}}, nil return []Content{NewTextContent("hi " + v.Name)}, nil
} }
func TestEndToEnd(t *testing.T) { func TestEndToEnd(t *testing.T) {
@ -53,7 +53,7 @@ func TestEndToEnd(t *testing.T) {
return &GetPromptResult{ return &GetPromptResult{
Description: "Code review prompt", Description: "Code review prompt",
Messages: []PromptMessage{ Messages: []PromptMessage{
{Role: "user", Content: TextContent{Text: "Please review the following code: " + params.Code}.ToWire()}, {Role: "user", Content: NewTextContent("Please review the following code: " + params.Code)},
}, },
}, nil }, nil
}), }),
@ -116,7 +116,7 @@ func TestEndToEnd(t *testing.T) {
wantReview := &GetPromptResult{ wantReview := &GetPromptResult{
Description: "Code review prompt", Description: "Code review prompt",
Messages: []PromptMessage{{ Messages: []PromptMessage{{
Content: TextContent{Text: "Please review the following code: 1+1"}.ToWire(), Content: NewTextContent("Please review the following code: 1+1"),
Role: "user", Role: "user",
}}, }},
} }
@ -165,7 +165,7 @@ func TestEndToEnd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
wantHi := &CallToolResult{ wantHi := &CallToolResult{
Content: []WireContent{{Type: "text", Text: "hi user"}}, Content: []Content{{Type: "text", Text: "hi user"}},
} }
if diff := cmp.Diff(wantHi, gotHi); diff != "" { if diff := cmp.Diff(wantHi, gotHi); diff != "" {
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff) t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
@ -179,7 +179,7 @@ func TestEndToEnd(t *testing.T) {
} }
wantFail := &CallToolResult{ wantFail := &CallToolResult{
IsError: true, IsError: true,
Content: []WireContent{{Type: "text", Text: failure.Error()}}, Content: []Content{{Type: "text", Text: failure.Error()}},
} }
if diff := cmp.Diff(wantFail, gotFail); diff != "" { if diff := cmp.Diff(wantFail, gotFail); diff != "" {
t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff) t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff)
@ -201,7 +201,7 @@ func TestEndToEnd(t *testing.T) {
readHandler := func(_ context.Context, r Resource, _ *ReadResourceParams) (*ReadResourceResult, error) { readHandler := func(_ context.Context, r Resource, _ *ReadResourceParams) (*ReadResourceResult, error) {
if r.URI == "file:///file1.txt" { if r.URI == "file:///file1.txt" {
return &ReadResourceResult{ return &ReadResourceResult{
Contents: &WireResource{ Contents: &ResourceContents{
Text: "file contents", Text: "file contents",
}, },
}, nil }, nil

View file

@ -47,7 +47,7 @@ type CallToolResult struct {
// This result property is reserved by the protocol to allow clients and servers // This result property is reserved by the protocol to allow clients and servers
// to attach additional metadata to their responses. // to attach additional metadata to their responses.
Meta map[string]json.RawMessage `json:"_meta,omitempty"` Meta map[string]json.RawMessage `json:"_meta,omitempty"`
Content []WireContent `json:"content"` Content []Content `json:"content"`
// Whether the tool call ended in an error. // Whether the tool call ended in an error.
// //
// If not set, this is assumed to be false (the call was successful). // If not set, this is assumed to be false (the call was successful).
@ -195,8 +195,8 @@ type PromptArgument struct {
// This is similar to `SamplingMessage`, but also supports the embedding of // This is similar to `SamplingMessage`, but also supports the embedding of
// resources from the MCP server. // resources from the MCP server.
type PromptMessage struct { type PromptMessage struct {
Content WireContent `json:"content"` Content Content `json:"content"`
Role Role `json:"role"` Role Role `json:"role"`
} }
type ReadResourceParams struct { type ReadResourceParams struct {
@ -210,7 +210,7 @@ type ReadResourceResult struct {
// This result property is reserved by the protocol to allow clients and servers // This result property is reserved by the protocol to allow clients and servers
// to attach additional metadata to their responses. // to attach additional metadata to their responses.
Meta map[string]json.RawMessage `json:"_meta,omitempty"` Meta map[string]json.RawMessage `json:"_meta,omitempty"`
Contents *WireResource `json:"contents"` Contents *ResourceContents `json:"contents"`
} }
// A known resource that the server is capable of reading. // A known resource that the server is capable of reading.

View file

@ -18,7 +18,7 @@ type SayHiParams struct {
func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *SayHiParams) ([]mcp.Content, error) { func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *SayHiParams) ([]mcp.Content, error) {
return []mcp.Content{ return []mcp.Content{
mcp.TextContent{Text: "Hi " + params.Name}, mcp.NewTextContent("Hi " + params.Name),
}, nil }, nil
} }

View file

@ -20,7 +20,7 @@ type AddParams struct {
func Add(ctx context.Context, cc *mcp.ServerConnection, params *AddParams) ([]mcp.Content, error) { func Add(ctx context.Context, cc *mcp.ServerConnection, params *AddParams) ([]mcp.Content, error) {
return []mcp.Content{ return []mcp.Content{
mcp.TextContent{Text: fmt.Sprintf("%d", params.X+params.Y)}, mcp.NewTextContent(fmt.Sprintf("%d", params.X+params.Y)),
}, nil }, nil
} }

View file

@ -48,7 +48,7 @@ func TestSSEServer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
wantHi := &CallToolResult{ wantHi := &CallToolResult{
Content: []WireContent{{Type: "text", Text: "hi user"}}, Content: []Content{{Type: "text", Text: "hi user"}},
} }
if diff := cmp.Diff(wantHi, gotHi); diff != "" { if diff := cmp.Diff(wantHi, gotHi); diff != "" {
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff) t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)

View file

@ -9,7 +9,6 @@ import (
"encoding/json" "encoding/json"
"slices" "slices"
"github.com/modelcontextprotocol/go-sdk/internal/util"
"github.com/modelcontextprotocol/go-sdk/jsonschema" "github.com/modelcontextprotocol/go-sdk/jsonschema"
) )
@ -56,12 +55,12 @@ func NewTool[TReq any](name, description string, handler func(context.Context, *
// rather than returned as jsonrpc2 server errors. // rather than returned as jsonrpc2 server errors.
if err != nil { if err != nil {
return &CallToolResult{ return &CallToolResult{
Content: []WireContent{TextContent{Text: err.Error()}.ToWire()}, Content: []Content{NewTextContent(err.Error())},
IsError: true, IsError: true,
}, nil }, nil
} }
res := &CallToolResult{ res := &CallToolResult{
Content: util.Apply(content, Content.ToWire), Content: content,
} }
return res, nil return res, nil
} }

View file

@ -14,11 +14,6 @@ func assert(cond bool, msg string) {
} }
} }
func is[T any](v any) bool {
_, ok := v.(T)
return ok
}
// Copied from crypto/rand. // Copied from crypto/rand.
// TODO: once 1.24 is assured, just use crypto/rand. // TODO: once 1.24 is assured, just use crypto/rand.
const base32alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" const base32alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"