diff --git a/examples/hello/main.go b/examples/hello/main.go index 7e3c52d..f733263 100644 --- a/examples/hello/main.go +++ b/examples/hello/main.go @@ -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 diff --git a/examples/sse/main.go b/examples/sse/main.go index 8ddea9e..e5b3b71 100644 --- a/examples/sse/main.go +++ b/examples/sse/main.go @@ -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 } diff --git a/jsonschema/schema.go b/jsonschema/schema.go index 821fe9f..76b971b 100644 --- a/jsonschema/schema.go +++ b/jsonschema/schema.go @@ -138,6 +138,14 @@ func (s *Schema) String() string { return "" } +// 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, "", " ") diff --git a/mcp/client.go b/mcp/client.go index b0f2756..8ca4600 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -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 diff --git a/mcp/cmd_test.go b/mcp/cmd_test.go index f380267..addc59e 100644 --- a/mcp/cmd_test.go +++ b/mcp/cmd_test.go @@ -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) diff --git a/mcp/content.go b/mcp/content.go index cac6d13..fcc0055 100644 --- a/mcp/content.go +++ b/mcp/content.go @@ -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. diff --git a/mcp/content_test.go b/mcp/content_test.go index 0f43eeb..24cb53c 100644 --- a/mcp/content_test.go +++ b/mcp/content_test.go @@ -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) } diff --git a/mcp/generate.go b/mcp/generate.go index 7724ec2..cec5b0e 100644 --- a/mcp/generate.go +++ b/mcp/generate.go @@ -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("", err) + } + return string(data) +} + // Map from initialism to the regexp that matches it. var initialisms = map[string]*regexp.Regexp{ "Id": nil, diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index 18eb797..acb638f 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -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(): diff --git a/mcp/prompt.go b/mcp/prompt.go index 2ec9228..08cc026 100644 --- a/mcp/prompt.go +++ b/mcp/prompt.go @@ -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] diff --git a/mcp/prompt_test.go b/mcp/prompt_test.go index 2da8f0f..c5f25a6 100644 --- a/mcp/prompt_test.go +++ b/mcp/prompt_test.go @@ -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"}, diff --git a/mcp/protocol.go b/mcp/protocol.go index e942ecd..ca36815 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -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 { diff --git a/mcp/server.go b/mcp/server.go index a404c74..9ea3214 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -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, }, diff --git a/mcp/server_example_test.go b/mcp/server_example_test.go index b94a07d..04042c1 100644 --- a/mcp/server_example_test.go +++ b/mcp/server_example_test.go @@ -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 } diff --git a/mcp/sse_example_test.go b/mcp/sse_example_test.go index 9b4e0fe..eee822f 100644 --- a/mcp/sse_example_test.go +++ b/mcp/sse_example_test.go @@ -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 } diff --git a/mcp/sse_test.go b/mcp/sse_test.go index ce8e514..ad45e64 100644 --- a/mcp/sse_test.go +++ b/mcp/sse_test.go @@ -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) diff --git a/mcp/tool.go b/mcp/tool.go index 86d0b0a..7a93c6c 100644 --- a/mcp/tool.go +++ b/mcp/tool.go @@ -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, diff --git a/mcp/tool_test.go b/mcp/tool_test.go index d23c610..fd3d05e 100644 --- a/mcp/tool_test.go +++ b/mcp/tool_test.go @@ -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") }