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

View file

@ -19,8 +19,8 @@ type SayHiParams struct {
Name string `json:"name" mcp:"the name to say hi to"`
}
func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *SayHiParams) ([]mcp.Content, error) {
return []mcp.Content{
func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *SayHiParams) ([]*mcp.Content, error) {
return []*mcp.Content{
mcp.NewTextContent("Hi " + params.Name),
}, nil
}

View file

@ -138,6 +138,14 @@ func (s *Schema) String() string {
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.
func (s *Schema) json() string {
data, err := json.MarshalIndent(s, "", " ")

View file

@ -24,7 +24,7 @@ type Client struct {
opts ClientOptions
mu sync.Mutex
conn *jsonrpc2.Connection
roots *featureSet[Root]
roots *featureSet[*Root]
initializeResult *initializeResult
}
@ -38,7 +38,7 @@ func NewClient(name, version string, t Transport, opts *ClientOptions) *Client {
name: name,
version: version,
transport: t,
roots: newFeatureSet(func(r Root) string { return r.URI }),
roots: newFeatureSet(func(r *Root) string { return r.URI }),
}
if opts != nil {
c.opts = *opts
@ -84,7 +84,7 @@ func (c *Client) Start(ctx context.Context) (err error) {
return err
}
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 {
return err
@ -112,7 +112,7 @@ func (c *Client) Wait() error {
// replacing any with the same URIs,
// and notifies any connected servers.
// TODO: notification
func (c *Client) AddRoots(roots ...Root) {
func (c *Client) AddRoots(roots ...*Root) {
c.mu.Lock()
defer c.mu.Unlock()
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.
func (c *Client) ListPrompts(ctx context.Context) ([]Prompt, error) {
func (c *Client) ListPrompts(ctx context.Context) ([]*Prompt, error) {
var (
params = &ListPromptsParams{}
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.
func (c *Client) ListTools(ctx context.Context) ([]Tool, error) {
func (c *Client) ListTools(ctx context.Context) ([]*Tool, error) {
var (
params = &ListToolsParams{}
result ListToolsResult

View file

@ -57,7 +57,7 @@ func TestCmdTransport(t *testing.T) {
log.Fatal(err)
}
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 != "" {
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.
func NewTextContent(text string) Content {
return Content{Type: "text", Text: text}
func NewTextContent(text string) *Content {
return &Content{Type: "text", Text: text}
}
// NewImageContent creates a [Content] with image data.
func NewImageContent(data []byte, mimeType string) Content {
return Content{Type: "image", Data: data, MIMEType: mimeType}
func NewImageContent(data []byte, mimeType string) *Content {
return &Content{Type: "image", Data: data, MIMEType: mimeType}
}
// NewAudioContent creates a [Content] with audio data.
func NewAudioContent(data []byte, mimeType string) Content {
return Content{Type: "audio", Data: data, MIMEType: mimeType}
func NewAudioContent(data []byte, mimeType string) *Content {
return &Content{Type: "audio", Data: data, MIMEType: mimeType}
}
// NewResourceContent creates a [Content] with an embedded resource.
func NewResourceContent(resource *ResourceContents) Content {
return Content{Type: "resource", Resource: resource}
func NewResourceContent(resource *ResourceContents) *Content {
return &Content{Type: "resource", Resource: resource}
}
// ResourceContents represents the union of the spec's {Text,Blob}ResourceContents types.

View file

@ -14,7 +14,7 @@ import (
func TestContent(t *testing.T) {
tests := []struct {
in mcp.Content
in *mcp.Content
want string // json serialization
}{
{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 != "" {
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 {
t.Fatal(err)
}

View file

@ -139,6 +139,10 @@ func main() {
if err := json.Unmarshal(data, &schema); err != nil {
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
// 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.
func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named map[string]*bytes.Buffer) error {
// 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
// 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.
@ -266,6 +271,9 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
} else if cfg.Name != "" {
name = cfg.Name
}
if isStruct(resolved) {
w.Write([]byte{'*'})
}
}
w.Write([]byte(name))
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 {
return s.Ref == "#/definitions/TextContent"
}) {
fmt.Fprintf(w, "Content")
fmt.Fprintf(w, "*Content")
} else {
// E.g. union types.
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)
// 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.
//
// TODO: use omitzero when available.
needPointer := !required &&
(strings.HasPrefix(fieldDef.Ref, "#/definitions/") ||
fieldDef.Type == "object" && !canHaveAdditionalProperties(fieldDef))
// This also future-proofs against the struct getting large.
fieldTypeSchema := fieldDef
// If the schema is a reference, dereference it.
if _, rs := deref(fieldDef); rs != nil {
fieldTypeSchema = rs
}
needPointer := isStruct(fieldTypeSchema)
if config != nil && config.Fields[export] != nil {
r := config.Fields[export]
if r.Substitute != "" {
@ -348,13 +357,8 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
}
fmt.Fprintf(w, typename)
}
} else {
if needPointer {
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)
}
} else 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)
if !required {
@ -452,6 +456,33 @@ func exportName(s string) string {
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.
var initialisms = map[string]*regexp.Regexp{
"Id": nil,

View file

@ -24,11 +24,11 @@ type hiParams struct {
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 {
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) {
@ -43,7 +43,7 @@ func TestEndToEnd(t *testing.T) {
// The 'fail' tool returns this error.
failure := errors.New("mcp failure")
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
}),
)
@ -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) {
return &GetPromptResult{
Description: "Code review prompt",
Messages: []PromptMessage{
Messages: []*PromptMessage{
{Role: "user", Content: NewTextContent("Please review the following code: " + params.Code)},
},
}, nil
@ -82,7 +82,7 @@ func TestEndToEnd(t *testing.T) {
}()
c := NewClient("testClient", "v1.0.0", ct, nil)
c.AddRoots(Root{URI: "file:///root"})
c.AddRoots(&Root{URI: "file:///root"})
// Connect the client.
if err := c.Start(ctx); err != nil {
@ -97,11 +97,11 @@ func TestEndToEnd(t *testing.T) {
if err != nil {
t.Errorf("prompts/list failed: %v", err)
}
wantPrompts := []Prompt{
wantPrompts := []*Prompt{
{
Name: "code_review",
Description: "do a code review",
Arguments: []PromptArgument{{Name: "Code", Required: true}},
Arguments: []*PromptArgument{{Name: "Code", Required: true}},
},
{Name: "fail"},
}
@ -115,7 +115,7 @@ func TestEndToEnd(t *testing.T) {
}
wantReview := &GetPromptResult{
Description: "Code review prompt",
Messages: []PromptMessage{{
Messages: []*PromptMessage{{
Content: NewTextContent("Please review the following code: 1+1"),
Role: "user",
}},
@ -134,7 +134,7 @@ func TestEndToEnd(t *testing.T) {
if err != nil {
t.Errorf("tools/list failed: %v", err)
}
wantTools := []Tool{
wantTools := []*Tool{
{
Name: "fail",
Description: "just fail",
@ -165,7 +165,7 @@ func TestEndToEnd(t *testing.T) {
t.Fatal(err)
}
wantHi := &CallToolResult{
Content: []Content{{Type: "text", Text: "hi user"}},
Content: []*Content{{Type: "text", Text: "hi user"}},
}
if diff := cmp.Diff(wantHi, gotHi); diff != "" {
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
@ -179,7 +179,7 @@ func TestEndToEnd(t *testing.T) {
}
wantFail := &CallToolResult{
IsError: true,
Content: []Content{{Type: "text", Text: failure.Error()}},
Content: []*Content{{Type: "text", Text: failure.Error()}},
}
if diff := cmp.Diff(wantFail, gotFail); 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) {
resource1 := Resource{
resource1 := &Resource{
Name: "public",
MIMEType: "text/plain",
URI: "file:///file1.txt",
}
resource2 := Resource{
resource2 := &Resource{
Name: "public", // names are not unique IDs
MIMEType: "text/plain",
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" {
return &ReadResourceResult{
Contents: &ResourceContents{
@ -216,7 +216,7 @@ func TestEndToEnd(t *testing.T) {
if err != nil {
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)
}
@ -370,7 +370,7 @@ func TestCancellation(t *testing.T) {
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{}{}
select {
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.
type ServerPrompt struct {
Definition Prompt
Definition *Prompt
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"))
}
prompt := &ServerPrompt{
Definition: Prompt{
Definition: &Prompt{
Name: name,
Description: description,
},
@ -54,7 +54,7 @@ func NewPrompt[TReq any](name, description string, handler func(context.Context,
if prop.Type != "string" {
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,
Description: prop.Description,
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.
func Argument(name string, opts ...SchemaOption) PromptOption {
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
})
var arg PromptArgument
var arg *PromptArgument
if i < 0 {
i = len(p.Definition.Arguments)
arg = PromptArgument{Name: name}
arg = &PromptArgument{Name: name}
p.Definition.Arguments = append(p.Definition.Arguments, arg)
} else {
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) {
tests := []struct {
prompt *mcp.ServerPrompt
want []mcp.PromptArgument
want []*mcp.PromptArgument
}{
{
mcp.NewPrompt("empty", "", testPromptHandler[struct{}]),
@ -28,7 +28,7 @@ func TestNewPrompt(t *testing.T) {
},
{
mcp.NewPrompt("add_arg", "", testPromptHandler[struct{}], mcp.Argument("x")),
[]mcp.PromptArgument{{Name: "x"}},
[]*mcp.PromptArgument{{Name: "x"}},
},
{
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("State", mcp.Required(false))),
[]mcp.PromptArgument{
[]*mcp.PromptArgument{
{Name: "State"},
{Name: "country"},
{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
// to attach additional metadata to their responses.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
Content []Content `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).
@ -73,7 +73,7 @@ type ClientCapabilities struct {
Experimental map[string]struct {
} `json:"experimental,omitempty"`
// Present if the client supports listing roots.
Roots *struct {
Roots struct {
// Whether the client supports notifications for changes to the roots list.
ListChanged bool `json:"listChanged,omitempty"`
} `json:"roots,omitempty"`
@ -95,8 +95,8 @@ type GetPromptResult struct {
// to attach additional metadata to their responses.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// An optional description for the prompt.
Description string `json:"description,omitempty"`
Messages []PromptMessage `json:"messages"`
Description string `json:"description,omitempty"`
Messages []*PromptMessage `json:"messages"`
}
type ListPromptsParams struct {
@ -112,8 +112,8 @@ type ListPromptsResult struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// An opaque token representing the pagination position after the last returned
// result. If present, there may be more results available.
NextCursor string `json:"nextCursor,omitempty"`
Prompts []Prompt `json:"prompts"`
NextCursor string `json:"nextCursor,omitempty"`
Prompts []*Prompt `json:"prompts"`
}
type ListResourcesParams struct {
@ -129,17 +129,17 @@ type ListResourcesResult struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// An opaque token representing the pagination position after the last returned
// result. If present, there may be more results available.
NextCursor string `json:"nextCursor,omitempty"`
Resources []Resource `json:"resources"`
NextCursor string `json:"nextCursor,omitempty"`
Resources []*Resource `json:"resources"`
}
type ListRootsParams struct {
Meta *struct {
Meta struct {
// If specified, the caller is requesting out-of-band progress notifications for
// this request (as represented by notifications/progress). The value of this
// parameter is an opaque token that will be attached to any subsequent
// notifications. The receiver is not obligated to provide these notifications.
ProgressToken *any `json:"progressToken,omitempty"`
ProgressToken any `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}
@ -150,7 +150,7 @@ type ListRootsResult struct {
// This result property is reserved by the protocol to allow clients and servers
// to attach additional metadata to their responses.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
Roots []Root `json:"roots"`
Roots []*Root `json:"roots"`
}
type ListToolsParams struct {
@ -166,14 +166,14 @@ type ListToolsResult struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// An opaque token representing the pagination position after the last returned
// result. If present, there may be more results available.
NextCursor string `json:"nextCursor,omitempty"`
Tools []Tool `json:"tools"`
NextCursor string `json:"nextCursor,omitempty"`
Tools []*Tool `json:"tools"`
}
// A prompt or prompt template that the server offers.
type Prompt struct {
// 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
Description string `json:"description,omitempty"`
// 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
// resources from the MCP server.
type PromptMessage struct {
Content Content `json:"content"`
Role Role `json:"role"`
Content *Content `json:"content"`
Role Role `json:"role"`
}
type ReadResourceParams struct {
@ -312,8 +312,8 @@ type implementation struct {
}
type initializeParams struct {
Capabilities ClientCapabilities `json:"capabilities"`
ClientInfo implementation `json:"clientInfo"`
Capabilities *ClientCapabilities `json:"capabilities"`
ClientInfo *implementation `json:"clientInfo"`
// The latest version of the Model Context Protocol that the client supports.
// The client MAY decide to support older versions as well.
ProtocolVersion string `json:"protocolVersion"`
@ -325,7 +325,7 @@ type initializeResult struct {
// This result property is reserved by the protocol to allow clients and servers
// to attach additional metadata to their responses.
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.
//
// 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
// may not match the version that the client requested. If the client cannot
// support this version, it MUST disconnect.
ProtocolVersion string `json:"protocolVersion"`
ServerInfo implementation `json:"serverInfo"`
ProtocolVersion string `json:"protocolVersion"`
ServerInfo *implementation `json:"serverInfo"`
}
type initializedParams struct {

View file

@ -121,11 +121,11 @@ const codeResourceNotFound = -31002
// A ReadResourceHandler is a function that reads a resource.
// 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.
type ServerResource struct {
Resource Resource
Resource *Resource
Handler ReadResourceHandler
}
@ -381,7 +381,7 @@ func (cc *ServerConnection) initialize(ctx context.Context, _ *ServerConnection,
return &initializeResult{
// TODO(rfindley): support multiple protocol versions.
ProtocolVersion: "2024-11-05",
Capabilities: serverCapabilities{
Capabilities: &serverCapabilities{
Prompts: &promptCapabilities{
ListChanged: false, // not yet supported
},
@ -390,7 +390,7 @@ func (cc *ServerConnection) initialize(ctx context.Context, _ *ServerConnection,
},
},
Instructions: cc.server.opts.Instructions,
ServerInfo: implementation{
ServerInfo: &implementation{
Name: cc.server.name,
Version: cc.server.version,
},

View file

@ -16,8 +16,8 @@ type SayHiParams struct {
Name string `json:"name" mcp:"the name to say hi to"`
}
func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *SayHiParams) ([]mcp.Content, error) {
return []mcp.Content{
func SayHi(ctx context.Context, cc *mcp.ServerConnection, params *SayHiParams) ([]*mcp.Content, error) {
return []*mcp.Content{
mcp.NewTextContent("Hi " + params.Name),
}, nil
}

View file

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

View file

@ -48,7 +48,7 @@ func TestSSEServer(t *testing.T) {
t.Fatal(err)
}
wantHi := &CallToolResult{
Content: []Content{{Type: "text", Text: "hi user"}},
Content: []*Content{{Type: "text", Text: "hi user"}},
}
if diff := cmp.Diff(wantHi, gotHi); 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.
type ServerTool struct {
Definition Tool
Definition *Tool
Handler ToolHandler
}
@ -34,7 +34,7 @@ type ServerTool struct {
//
// TODO: just have the handler return a CallToolResult: returning []Content is
// 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]()
if err != nil {
panic(err)
@ -55,7 +55,7 @@ func NewTool[TReq any](name, description string, handler func(context.Context, *
// rather than returned as jsonrpc2 server errors.
if err != nil {
return &CallToolResult{
Content: []Content{NewTextContent(err.Error())},
Content: []*Content{NewTextContent(err.Error())},
IsError: true,
}, nil
}
@ -65,7 +65,7 @@ func NewTool[TReq any](name, description string, handler func(context.Context, *
return res, nil
}
t := &ServerTool{
Definition: Tool{
Definition: &Tool{
Name: name,
Description: description,
InputSchema: schema,

View file

@ -15,7 +15,7 @@ import (
)
// 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")
}