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:
parent
7b100d7b30
commit
02fa1b3ea6
18 changed files with 137 additions and 98 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, "", " ")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue