internal/mcp: pointerize all the things

Use pointers for the protocol's struct types, to future-proof against
them getting large.

Change-Id: I55b204080a41c8140539697ba264e502e912e613
Reviewed-on: https://go-review.googlesource.com/c/tools/+/672575
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Jonathan Amsterdam 2025-05-13 19:10:34 -04:00
commit 02fa1b3ea6
18 changed files with 137 additions and 98 deletions

View file

@ -20,8 +20,8 @@ type HiParams struct {
Name string `json:"name"` Name string `json:"name"`
} }
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.NewTextContent("Hi " + params.Name), mcp.NewTextContent("Hi " + params.Name),
}, nil }, nil
} }
@ -29,7 +29,7 @@ func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *HiParams) ([]m
func PromptHi(ctx context.Context, cc *mcp.ServerConnection, params *HiParams) (*mcp.GetPromptResult, error) { func PromptHi(ctx context.Context, cc *mcp.ServerConnection, params *HiParams) (*mcp.GetPromptResult, error) {
return &mcp.GetPromptResult{ return &mcp.GetPromptResult{
Description: "Code review prompt", Description: "Code review prompt",
Messages: []mcp.PromptMessage{ Messages: []*mcp.PromptMessage{
{Role: "user", Content: mcp.NewTextContent("Say hi to " + params.Name)}, {Role: "user", Content: mcp.NewTextContent("Say hi to " + params.Name)},
}, },
}, nil }, nil

View file

@ -19,8 +19,8 @@ type SayHiParams struct {
Name string `json:"name" mcp:"the name to say hi to"` Name string `json:"name" mcp:"the name to say hi to"`
} }
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.NewTextContent("Hi " + params.Name), mcp.NewTextContent("Hi " + params.Name),
}, nil }, nil
} }

View file

@ -138,6 +138,14 @@ func (s *Schema) String() string {
return "<anonymous schema>" return "<anonymous schema>"
} }
// ResolvedRef returns the Schema to which this schema's $ref keyword
// refers, or nil if it doesn't have a $ref.
// It returns nil if this schema has not been resolved, meaning that
// [Schema.Resolve] was called on it or one of its ancestors.
func (s *Schema) ResolvedRef() *Schema {
return s.resolvedRef
}
// json returns the schema in json format. // json returns the schema in json format.
func (s *Schema) json() string { func (s *Schema) json() string {
data, err := json.MarshalIndent(s, "", " ") data, err := json.MarshalIndent(s, "", " ")

View file

@ -24,7 +24,7 @@ type Client struct {
opts ClientOptions opts ClientOptions
mu sync.Mutex mu sync.Mutex
conn *jsonrpc2.Connection conn *jsonrpc2.Connection
roots *featureSet[Root] roots *featureSet[*Root]
initializeResult *initializeResult initializeResult *initializeResult
} }
@ -38,7 +38,7 @@ func NewClient(name, version string, t Transport, opts *ClientOptions) *Client {
name: name, name: name,
version: version, version: version,
transport: t, transport: t,
roots: newFeatureSet(func(r Root) string { return r.URI }), roots: newFeatureSet(func(r *Root) string { return r.URI }),
} }
if opts != nil { if opts != nil {
c.opts = *opts c.opts = *opts
@ -84,7 +84,7 @@ func (c *Client) Start(ctx context.Context) (err error) {
return err return err
} }
params := &initializeParams{ params := &initializeParams{
ClientInfo: implementation{Name: c.name, Version: c.version}, ClientInfo: &implementation{Name: c.name, Version: c.version},
} }
if err := call(ctx, c.conn, "initialize", params, &c.initializeResult); err != nil { if err := call(ctx, c.conn, "initialize", params, &c.initializeResult); err != nil {
return err return err
@ -112,7 +112,7 @@ func (c *Client) Wait() error {
// replacing any with the same URIs, // replacing any with the same URIs,
// and notifies any connected servers. // and notifies any connected servers.
// TODO: notification // TODO: notification
func (c *Client) AddRoots(roots ...Root) { func (c *Client) AddRoots(roots ...*Root) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.roots.add(roots...) c.roots.add(roots...)
@ -159,7 +159,7 @@ func (c *Client) Ping(ctx context.Context) error {
} }
// ListPrompts lists prompts that are currently available on the server. // ListPrompts lists prompts that are currently available on the server.
func (c *Client) ListPrompts(ctx context.Context) ([]Prompt, error) { func (c *Client) ListPrompts(ctx context.Context) ([]*Prompt, error) {
var ( var (
params = &ListPromptsParams{} params = &ListPromptsParams{}
result ListPromptsResult result ListPromptsResult
@ -186,7 +186,7 @@ func (c *Client) GetPrompt(ctx context.Context, name string, args map[string]str
} }
// ListTools lists tools that are currently available on the server. // ListTools lists tools that are currently available on the server.
func (c *Client) ListTools(ctx context.Context) ([]Tool, error) { func (c *Client) ListTools(ctx context.Context) ([]*Tool, error) {
var ( var (
params = &ListToolsParams{} params = &ListToolsParams{}
result ListToolsResult result ListToolsResult

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.Content{{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

@ -45,23 +45,23 @@ func (c *Content) UnmarshalJSON(data []byte) error {
} }
// NewTextContent creates a [Content] with text. // NewTextContent creates a [Content] with text.
func NewTextContent(text string) Content { func NewTextContent(text string) *Content {
return Content{Type: "text", Text: text} return &Content{Type: "text", Text: text}
} }
// NewImageContent creates a [Content] with image data. // NewImageContent creates a [Content] with image data.
func NewImageContent(data []byte, mimeType string) Content { func NewImageContent(data []byte, mimeType string) *Content {
return Content{Type: "image", Data: data, MIMEType: mimeType} return &Content{Type: "image", Data: data, MIMEType: mimeType}
} }
// NewAudioContent creates a [Content] with audio data. // NewAudioContent creates a [Content] with audio data.
func NewAudioContent(data []byte, mimeType string) Content { func NewAudioContent(data []byte, mimeType string) *Content {
return Content{Type: "audio", Data: data, MIMEType: mimeType} return &Content{Type: "audio", Data: data, MIMEType: mimeType}
} }
// NewResourceContent creates a [Content] with an embedded resource. // NewResourceContent creates a [Content] with an embedded resource.
func NewResourceContent(resource *ResourceContents) Content { func NewResourceContent(resource *ResourceContents) *Content {
return Content{Type: "resource", Resource: resource} return &Content{Type: "resource", Resource: resource}
} }
// ResourceContents represents the union of the spec's {Text,Blob}ResourceContents types. // ResourceContents represents the union of the spec's {Text,Blob}ResourceContents types.

View file

@ -14,7 +14,7 @@ import (
func TestContent(t *testing.T) { func TestContent(t *testing.T) {
tests := []struct { tests := []struct {
in mcp.Content in *mcp.Content
want string // json serialization want string // json serialization
}{ }{
{mcp.NewTextContent("hello"), `{"type":"text","text":"hello"}`}, {mcp.NewTextContent("hello"), `{"type":"text","text":"hello"}`},
@ -48,7 +48,7 @@ func TestContent(t *testing.T) {
if diff := cmp.Diff(test.want, string(got)); diff != "" { if diff := cmp.Diff(test.want, string(got)); diff != "" {
t.Errorf("json.Marshal(%v) mismatch (-want +got):\n%s", test.in, diff) t.Errorf("json.Marshal(%v) mismatch (-want +got):\n%s", test.in, diff)
} }
var out mcp.Content var out *mcp.Content
if err := json.Unmarshal(got, &out); err != nil { if err := json.Unmarshal(got, &out); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -139,6 +139,10 @@ func main() {
if err := json.Unmarshal(data, &schema); err != nil { if err := json.Unmarshal(data, &schema); err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Resolve the schema so we have the referents of all the Refs.
if _, err := schema.Resolve("", nil); err != nil {
log.Fatal(err)
}
// Collect named types. Since we may create new type definitions while // Collect named types. Since we may create new type definitions while
// writing types, we collect definitions and concatenate them later. This // writing types, we collect definitions and concatenate them later. This
@ -253,7 +257,8 @@ func writeDecl(configName string, config typeConfig, def *jsonschema.Schema, nam
// be added during writeType, if they are extracted from inner fields. // be added during writeType, if they are extracted from inner fields.
func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named map[string]*bytes.Buffer) error { func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named map[string]*bytes.Buffer) error {
// Use type names for Named types. // Use type names for Named types.
if name := strings.TrimPrefix(def.Ref, "#/definitions/"); name != "" { name, resolved := deref(def)
if name != "" {
// TODO: this check is not quite right: we should really panic if the // TODO: this check is not quite right: we should really panic if the
// definition is missing, *but only if w is not io.Discard*. That's not a // definition is missing, *but only if w is not io.Discard*. That's not a
// great API: see if we can do something more explicit than io.Discard. // great API: see if we can do something more explicit than io.Discard.
@ -266,6 +271,9 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
} else if cfg.Name != "" { } else if cfg.Name != "" {
name = cfg.Name name = cfg.Name
} }
if isStruct(resolved) {
w.Write([]byte{'*'})
}
} }
w.Write([]byte(name)) w.Write([]byte(name))
return nil return nil
@ -288,7 +296,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, "Content") fmt.Fprintf(w, "*Content")
} else { } else {
// E.g. union types. // E.g. union types.
fmt.Fprintf(w, "json.RawMessage") fmt.Fprintf(w, "json.RawMessage")
@ -322,14 +330,15 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
required := slices.Contains(def.Required, name) required := slices.Contains(def.Required, name)
// If the field is not required, and is a struct type, indirect with a // If the field is a struct type, indirect with a
// pointer so that it can be empty as defined by encoding/json. // pointer so that it can be empty as defined by encoding/json.
// // This also future-proofs against the struct getting large.
// TODO: use omitzero when available. fieldTypeSchema := fieldDef
needPointer := !required && // If the schema is a reference, dereference it.
(strings.HasPrefix(fieldDef.Ref, "#/definitions/") || if _, rs := deref(fieldDef); rs != nil {
fieldDef.Type == "object" && !canHaveAdditionalProperties(fieldDef)) fieldTypeSchema = rs
}
needPointer := isStruct(fieldTypeSchema)
if config != nil && config.Fields[export] != nil { if config != nil && config.Fields[export] != nil {
r := config.Fields[export] r := config.Fields[export]
if r.Substitute != "" { if r.Substitute != "" {
@ -348,13 +357,8 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
} }
fmt.Fprintf(w, typename) fmt.Fprintf(w, typename)
} }
} else { } else if err := writeType(w, nil, fieldDef, named); err != nil {
if needPointer { return fmt.Errorf("failed to write type for field %s: %v", export, err)
fmt.Fprintf(w, "*")
}
if err := writeType(w, nil, fieldDef, named); err != nil {
return fmt.Errorf("failed to write type for field %s: %v", export, err)
}
} }
fmt.Fprintf(w, " `json:\"%s", name) fmt.Fprintf(w, " `json:\"%s", name)
if !required { if !required {
@ -452,6 +456,33 @@ func exportName(s string) string {
return s return s
} }
// deref dereferences s.Ref.
// If s.Ref refers to a schema in the Definitions section, deref
// returns the definition name and the associated schema.
// Otherwise, deref returns "", nil.
func deref(s *jsonschema.Schema) (name string, _ *jsonschema.Schema) {
name, ok := strings.CutPrefix(s.Ref, "#/definitions/")
if !ok {
return "", nil
}
return name, s.ResolvedRef()
}
// isStruct reports whether s should be translated to a struct.
func isStruct(s *jsonschema.Schema) bool {
return s.Type == "object" && s.Properties != nil && !canHaveAdditionalProperties(s)
}
// schemaJSON returns the JSON for s.
// For debugging.
func schemaJSON(s *jsonschema.Schema) string {
data, err := json.Marshal(s)
if err != nil {
return fmt.Sprintf("<jsonschema.Schema:%v>", err)
}
return string(data)
}
// Map from initialism to the regexp that matches it. // Map from initialism to the regexp that matches it.
var initialisms = map[string]*regexp.Regexp{ var initialisms = map[string]*regexp.Regexp{
"Id": nil, "Id": nil,

View file

@ -24,11 +24,11 @@ type hiParams struct {
Name string Name string
} }
func sayHi(ctx context.Context, cc *ServerConnection, v hiParams) ([]Content, error) { func sayHi(ctx context.Context, cc *ServerConnection, v hiParams) ([]*Content, error) {
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{NewTextContent("hi " + v.Name)}, nil return []*Content{NewTextContent("hi " + v.Name)}, nil
} }
func TestEndToEnd(t *testing.T) { func TestEndToEnd(t *testing.T) {
@ -43,7 +43,7 @@ func TestEndToEnd(t *testing.T) {
// The 'fail' tool returns this error. // The 'fail' tool returns this error.
failure := errors.New("mcp failure") failure := errors.New("mcp failure")
s.AddTools( s.AddTools(
NewTool("fail", "just fail", func(context.Context, *ServerConnection, struct{}) ([]Content, error) { NewTool("fail", "just fail", func(context.Context, *ServerConnection, struct{}) ([]*Content, error) {
return nil, failure return nil, failure
}), }),
) )
@ -52,7 +52,7 @@ func TestEndToEnd(t *testing.T) {
NewPrompt("code_review", "do a code review", func(_ context.Context, _ *ServerConnection, params struct{ Code string }) (*GetPromptResult, error) { NewPrompt("code_review", "do a code review", func(_ context.Context, _ *ServerConnection, params struct{ Code string }) (*GetPromptResult, error) {
return &GetPromptResult{ return &GetPromptResult{
Description: "Code review prompt", Description: "Code review prompt",
Messages: []PromptMessage{ Messages: []*PromptMessage{
{Role: "user", Content: NewTextContent("Please review the following code: " + params.Code)}, {Role: "user", Content: NewTextContent("Please review the following code: " + params.Code)},
}, },
}, nil }, nil
@ -82,7 +82,7 @@ func TestEndToEnd(t *testing.T) {
}() }()
c := NewClient("testClient", "v1.0.0", ct, nil) c := NewClient("testClient", "v1.0.0", ct, nil)
c.AddRoots(Root{URI: "file:///root"}) c.AddRoots(&Root{URI: "file:///root"})
// Connect the client. // Connect the client.
if err := c.Start(ctx); err != nil { if err := c.Start(ctx); err != nil {
@ -97,11 +97,11 @@ func TestEndToEnd(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("prompts/list failed: %v", err) t.Errorf("prompts/list failed: %v", err)
} }
wantPrompts := []Prompt{ wantPrompts := []*Prompt{
{ {
Name: "code_review", Name: "code_review",
Description: "do a code review", Description: "do a code review",
Arguments: []PromptArgument{{Name: "Code", Required: true}}, Arguments: []*PromptArgument{{Name: "Code", Required: true}},
}, },
{Name: "fail"}, {Name: "fail"},
} }
@ -115,7 +115,7 @@ func TestEndToEnd(t *testing.T) {
} }
wantReview := &GetPromptResult{ wantReview := &GetPromptResult{
Description: "Code review prompt", Description: "Code review prompt",
Messages: []PromptMessage{{ Messages: []*PromptMessage{{
Content: NewTextContent("Please review the following code: 1+1"), Content: NewTextContent("Please review the following code: 1+1"),
Role: "user", Role: "user",
}}, }},
@ -134,7 +134,7 @@ func TestEndToEnd(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("tools/list failed: %v", err) t.Errorf("tools/list failed: %v", err)
} }
wantTools := []Tool{ wantTools := []*Tool{
{ {
Name: "fail", Name: "fail",
Description: "just fail", Description: "just fail",
@ -165,7 +165,7 @@ func TestEndToEnd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
wantHi := &CallToolResult{ wantHi := &CallToolResult{
Content: []Content{{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: []Content{{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)
@ -187,18 +187,18 @@ func TestEndToEnd(t *testing.T) {
}) })
t.Run("resources", func(t *testing.T) { t.Run("resources", func(t *testing.T) {
resource1 := Resource{ resource1 := &Resource{
Name: "public", Name: "public",
MIMEType: "text/plain", MIMEType: "text/plain",
URI: "file:///file1.txt", URI: "file:///file1.txt",
} }
resource2 := Resource{ resource2 := &Resource{
Name: "public", // names are not unique IDs Name: "public", // names are not unique IDs
MIMEType: "text/plain", MIMEType: "text/plain",
URI: "file:///nonexistent.txt", URI: "file:///nonexistent.txt",
} }
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: &ResourceContents{ Contents: &ResourceContents{
@ -216,7 +216,7 @@ func TestEndToEnd(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if diff := cmp.Diff([]Resource{resource1, resource2}, lrres.Resources); diff != "" { if diff := cmp.Diff([]*Resource{resource1, resource2}, lrres.Resources); diff != "" {
t.Errorf("resources/list mismatch (-want, +got):\n%s", diff) t.Errorf("resources/list mismatch (-want, +got):\n%s", diff)
} }
@ -370,7 +370,7 @@ func TestCancellation(t *testing.T) {
cancelled = make(chan struct{}, 1) // don't block the request cancelled = make(chan struct{}, 1) // don't block the request
) )
slowRequest := func(ctx context.Context, cc *ServerConnection, v struct{}) ([]Content, error) { slowRequest := func(ctx context.Context, cc *ServerConnection, v struct{}) ([]*Content, error) {
start <- struct{}{} start <- struct{}{}
select { select {
case <-ctx.Done(): case <-ctx.Done():

View file

@ -20,7 +20,7 @@ type PromptHandler func(context.Context, *ServerConnection, map[string]string) (
// A Prompt is a prompt definition bound to a prompt handler. // A Prompt is a prompt definition bound to a prompt handler.
type ServerPrompt struct { type ServerPrompt struct {
Definition Prompt Definition *Prompt
Handler PromptHandler Handler PromptHandler
} }
@ -41,7 +41,7 @@ func NewPrompt[TReq any](name, description string, handler func(context.Context,
panic(fmt.Sprintf("handler request type must be a struct")) panic(fmt.Sprintf("handler request type must be a struct"))
} }
prompt := &ServerPrompt{ prompt := &ServerPrompt{
Definition: Prompt{ Definition: &Prompt{
Name: name, Name: name,
Description: description, Description: description,
}, },
@ -54,7 +54,7 @@ func NewPrompt[TReq any](name, description string, handler func(context.Context,
if prop.Type != "string" { if prop.Type != "string" {
panic(fmt.Sprintf("handler type must consist only of string fields")) panic(fmt.Sprintf("handler type must consist only of string fields"))
} }
prompt.Definition.Arguments = append(prompt.Definition.Arguments, PromptArgument{ prompt.Definition.Arguments = append(prompt.Definition.Arguments, &PromptArgument{
Name: name, Name: name,
Description: prop.Description, Description: prop.Description,
Required: required[name], Required: required[name],
@ -95,13 +95,13 @@ func (s promptSetter) set(p *ServerPrompt) { s(p) }
// Required and Description, and panics when encountering any other option. // Required and Description, and panics when encountering any other option.
func Argument(name string, opts ...SchemaOption) PromptOption { func Argument(name string, opts ...SchemaOption) PromptOption {
return promptSetter(func(p *ServerPrompt) { return promptSetter(func(p *ServerPrompt) {
i := slices.IndexFunc(p.Definition.Arguments, func(arg PromptArgument) bool { i := slices.IndexFunc(p.Definition.Arguments, func(arg *PromptArgument) bool {
return arg.Name == name return arg.Name == name
}) })
var arg PromptArgument var arg *PromptArgument
if i < 0 { if i < 0 {
i = len(p.Definition.Arguments) i = len(p.Definition.Arguments)
arg = PromptArgument{Name: name} arg = &PromptArgument{Name: name}
p.Definition.Arguments = append(p.Definition.Arguments, arg) p.Definition.Arguments = append(p.Definition.Arguments, arg)
} else { } else {
arg = p.Definition.Arguments[i] arg = p.Definition.Arguments[i]

View file

@ -20,7 +20,7 @@ func testPromptHandler[T any](context.Context, *mcp.ServerConnection, T) (*mcp.G
func TestNewPrompt(t *testing.T) { func TestNewPrompt(t *testing.T) {
tests := []struct { tests := []struct {
prompt *mcp.ServerPrompt prompt *mcp.ServerPrompt
want []mcp.PromptArgument want []*mcp.PromptArgument
}{ }{
{ {
mcp.NewPrompt("empty", "", testPromptHandler[struct{}]), mcp.NewPrompt("empty", "", testPromptHandler[struct{}]),
@ -28,7 +28,7 @@ func TestNewPrompt(t *testing.T) {
}, },
{ {
mcp.NewPrompt("add_arg", "", testPromptHandler[struct{}], mcp.Argument("x")), mcp.NewPrompt("add_arg", "", testPromptHandler[struct{}], mcp.Argument("x")),
[]mcp.PromptArgument{{Name: "x"}}, []*mcp.PromptArgument{{Name: "x"}},
}, },
{ {
mcp.NewPrompt("combo", "", testPromptHandler[struct { mcp.NewPrompt("combo", "", testPromptHandler[struct {
@ -38,7 +38,7 @@ func TestNewPrompt(t *testing.T) {
}], }],
mcp.Argument("name", mcp.Description("the person's name")), mcp.Argument("name", mcp.Description("the person's name")),
mcp.Argument("State", mcp.Required(false))), mcp.Argument("State", mcp.Required(false))),
[]mcp.PromptArgument{ []*mcp.PromptArgument{
{Name: "State"}, {Name: "State"},
{Name: "country"}, {Name: "country"},
{Name: "name", Required: true, Description: "the person's name"}, {Name: "name", Required: true, Description: "the person's name"},

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 []Content `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).
@ -73,7 +73,7 @@ type ClientCapabilities struct {
Experimental map[string]struct { Experimental map[string]struct {
} `json:"experimental,omitempty"` } `json:"experimental,omitempty"`
// Present if the client supports listing roots. // Present if the client supports listing roots.
Roots *struct { Roots struct {
// Whether the client supports notifications for changes to the roots list. // Whether the client supports notifications for changes to the roots list.
ListChanged bool `json:"listChanged,omitempty"` ListChanged bool `json:"listChanged,omitempty"`
} `json:"roots,omitempty"` } `json:"roots,omitempty"`
@ -95,8 +95,8 @@ type GetPromptResult struct {
// 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"`
// An optional description for the prompt. // An optional description for the prompt.
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Messages []PromptMessage `json:"messages"` Messages []*PromptMessage `json:"messages"`
} }
type ListPromptsParams struct { type ListPromptsParams struct {
@ -112,8 +112,8 @@ type ListPromptsResult struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"` Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// An opaque token representing the pagination position after the last returned // An opaque token representing the pagination position after the last returned
// result. If present, there may be more results available. // result. If present, there may be more results available.
NextCursor string `json:"nextCursor,omitempty"` NextCursor string `json:"nextCursor,omitempty"`
Prompts []Prompt `json:"prompts"` Prompts []*Prompt `json:"prompts"`
} }
type ListResourcesParams struct { type ListResourcesParams struct {
@ -129,17 +129,17 @@ type ListResourcesResult struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"` Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// An opaque token representing the pagination position after the last returned // An opaque token representing the pagination position after the last returned
// result. If present, there may be more results available. // result. If present, there may be more results available.
NextCursor string `json:"nextCursor,omitempty"` NextCursor string `json:"nextCursor,omitempty"`
Resources []Resource `json:"resources"` Resources []*Resource `json:"resources"`
} }
type ListRootsParams struct { type ListRootsParams struct {
Meta *struct { Meta struct {
// If specified, the caller is requesting out-of-band progress notifications for // If specified, the caller is requesting out-of-band progress notifications for
// this request (as represented by notifications/progress). The value of this // this request (as represented by notifications/progress). The value of this
// parameter is an opaque token that will be attached to any subsequent // parameter is an opaque token that will be attached to any subsequent
// notifications. The receiver is not obligated to provide these notifications. // notifications. The receiver is not obligated to provide these notifications.
ProgressToken *any `json:"progressToken,omitempty"` ProgressToken any `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"` } `json:"_meta,omitempty"`
} }
@ -150,7 +150,7 @@ type ListRootsResult 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"`
Roots []Root `json:"roots"` Roots []*Root `json:"roots"`
} }
type ListToolsParams struct { type ListToolsParams struct {
@ -166,14 +166,14 @@ type ListToolsResult struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"` Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// An opaque token representing the pagination position after the last returned // An opaque token representing the pagination position after the last returned
// result. If present, there may be more results available. // result. If present, there may be more results available.
NextCursor string `json:"nextCursor,omitempty"` NextCursor string `json:"nextCursor,omitempty"`
Tools []Tool `json:"tools"` Tools []*Tool `json:"tools"`
} }
// A prompt or prompt template that the server offers. // A prompt or prompt template that the server offers.
type Prompt struct { type Prompt struct {
// A list of arguments to use for templating the prompt. // A list of arguments to use for templating the prompt.
Arguments []PromptArgument `json:"arguments,omitempty"` Arguments []*PromptArgument `json:"arguments,omitempty"`
// An optional description of what this prompt provides // An optional description of what this prompt provides
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
// The name of the prompt or prompt template. // The name of the prompt or prompt template.
@ -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 Content `json:"content"` Content *Content `json:"content"`
Role Role `json:"role"` Role Role `json:"role"`
} }
type ReadResourceParams struct { type ReadResourceParams struct {
@ -312,8 +312,8 @@ type implementation struct {
} }
type initializeParams struct { type initializeParams struct {
Capabilities ClientCapabilities `json:"capabilities"` Capabilities *ClientCapabilities `json:"capabilities"`
ClientInfo implementation `json:"clientInfo"` ClientInfo *implementation `json:"clientInfo"`
// The latest version of the Model Context Protocol that the client supports. // The latest version of the Model Context Protocol that the client supports.
// The client MAY decide to support older versions as well. // The client MAY decide to support older versions as well.
ProtocolVersion string `json:"protocolVersion"` ProtocolVersion string `json:"protocolVersion"`
@ -325,7 +325,7 @@ type initializeResult 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"`
Capabilities serverCapabilities `json:"capabilities"` Capabilities *serverCapabilities `json:"capabilities"`
// Instructions describing how to use the server and its features. // Instructions describing how to use the server and its features.
// //
// This can be used by clients to improve the LLM's understanding of available // This can be used by clients to improve the LLM's understanding of available
@ -335,8 +335,8 @@ type initializeResult struct {
// The version of the Model Context Protocol that the server wants to use. This // The version of the Model Context Protocol that the server wants to use. This
// may not match the version that the client requested. If the client cannot // may not match the version that the client requested. If the client cannot
// support this version, it MUST disconnect. // support this version, it MUST disconnect.
ProtocolVersion string `json:"protocolVersion"` ProtocolVersion string `json:"protocolVersion"`
ServerInfo implementation `json:"serverInfo"` ServerInfo *implementation `json:"serverInfo"`
} }
type initializedParams struct { type initializedParams struct {

View file

@ -121,11 +121,11 @@ const codeResourceNotFound = -31002
// A ReadResourceHandler is a function that reads a resource. // A ReadResourceHandler is a function that reads a resource.
// If it cannot find the resource, it should return the result of calling [ResourceNotFoundError]. // If it cannot find the resource, it should return the result of calling [ResourceNotFoundError].
type ReadResourceHandler func(context.Context, Resource, *ReadResourceParams) (*ReadResourceResult, error) type ReadResourceHandler func(context.Context, *Resource, *ReadResourceParams) (*ReadResourceResult, error)
// A ServerResource associates a Resource with its handler. // A ServerResource associates a Resource with its handler.
type ServerResource struct { type ServerResource struct {
Resource Resource Resource *Resource
Handler ReadResourceHandler Handler ReadResourceHandler
} }
@ -381,7 +381,7 @@ func (cc *ServerConnection) initialize(ctx context.Context, _ *ServerConnection,
return &initializeResult{ return &initializeResult{
// TODO(rfindley): support multiple protocol versions. // TODO(rfindley): support multiple protocol versions.
ProtocolVersion: "2024-11-05", ProtocolVersion: "2024-11-05",
Capabilities: serverCapabilities{ Capabilities: &serverCapabilities{
Prompts: &promptCapabilities{ Prompts: &promptCapabilities{
ListChanged: false, // not yet supported ListChanged: false, // not yet supported
}, },
@ -390,7 +390,7 @@ func (cc *ServerConnection) initialize(ctx context.Context, _ *ServerConnection,
}, },
}, },
Instructions: cc.server.opts.Instructions, Instructions: cc.server.opts.Instructions,
ServerInfo: implementation{ ServerInfo: &implementation{
Name: cc.server.name, Name: cc.server.name,
Version: cc.server.version, Version: cc.server.version,
}, },

View file

@ -16,8 +16,8 @@ type SayHiParams struct {
Name string `json:"name" mcp:"the name to say hi to"` Name string `json:"name" mcp:"the name to say hi to"`
} }
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.NewTextContent("Hi " + params.Name), mcp.NewTextContent("Hi " + params.Name),
}, nil }, nil
} }

View file

@ -18,8 +18,8 @@ type AddParams struct {
X, Y int X, Y int
} }
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.NewTextContent(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: []Content{{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

@ -17,7 +17,7 @@ type ToolHandler func(context.Context, *ServerConnection, map[string]json.RawMes
// A Tool is a tool definition that is bound to a tool handler. // A Tool is a tool definition that is bound to a tool handler.
type ServerTool struct { type ServerTool struct {
Definition Tool Definition *Tool
Handler ToolHandler Handler ToolHandler
} }
@ -34,7 +34,7 @@ type ServerTool struct {
// //
// TODO: just have the handler return a CallToolResult: returning []Content is // TODO: just have the handler return a CallToolResult: returning []Content is
// going to be inconsistent with other server features. // going to be inconsistent with other server features.
func NewTool[TReq any](name, description string, handler func(context.Context, *ServerConnection, TReq) ([]Content, error), opts ...ToolOption) *ServerTool { func NewTool[TReq any](name, description string, handler func(context.Context, *ServerConnection, TReq) ([]*Content, error), opts ...ToolOption) *ServerTool {
schema, err := jsonschema.For[TReq]() schema, err := jsonschema.For[TReq]()
if err != nil { if err != nil {
panic(err) panic(err)
@ -55,7 +55,7 @@ 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: []Content{NewTextContent(err.Error())}, Content: []*Content{NewTextContent(err.Error())},
IsError: true, IsError: true,
}, nil }, nil
} }
@ -65,7 +65,7 @@ func NewTool[TReq any](name, description string, handler func(context.Context, *
return res, nil return res, nil
} }
t := &ServerTool{ t := &ServerTool{
Definition: Tool{ Definition: &Tool{
Name: name, Name: name,
Description: description, Description: description,
InputSchema: schema, InputSchema: schema,

View file

@ -15,7 +15,7 @@ import (
) )
// testToolHandler is used for type inference in TestNewTool. // testToolHandler is used for type inference in TestNewTool.
func testToolHandler[T any](context.Context, *mcp.ServerConnection, T) ([]mcp.Content, error) { func testToolHandler[T any](context.Context, *mcp.ServerConnection, T) ([]*mcp.Content, error) {
panic("not implemented") panic("not implemented")
} }