internal/mcp: support prompts
Add support for 'prompts/list' and 'prompts/get', and registering prompts on the server using AddPrompts. Add a 'MakePrompt' helper to construct prompts from a prompt handler using reflection. Change-Id: I479cb9c9b99313cca99640c1f5ea1939363759e6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/669015 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
parent
e082a340be
commit
af4d138732
17 changed files with 531 additions and 98 deletions
|
|
@ -6,26 +6,46 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/internal/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
var httpAddr = flag.String("http", "", "if set, use SSE HTTP at this address, instead of stdin/stdout")
|
var httpAddr = flag.String("http", "", "if set, use SSE HTTP at this address, instead of stdin/stdout")
|
||||||
|
|
||||||
type SayHiParams struct {
|
type HiParams struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func SayHi(ctx context.Context, cc *mcp.ClientConnection, params *SayHiParams) ([]mcp.Content, error) {
|
func SayHi(ctx context.Context, cc *mcp.ClientConnection, params *HiParams) ([]mcp.Content, error) {
|
||||||
return []mcp.Content{
|
return []mcp.Content{
|
||||||
mcp.TextContent{Text: "Hi " + params.Name},
|
mcp.TextContent{Text: "Hi " + params.Name},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PromptHi(ctx context.Context, cc *mcp.ClientConnection, params *HiParams) (*protocol.GetPromptResult, error) {
|
||||||
|
// (see related TODOs about cleaning up content construction)
|
||||||
|
content, err := json.Marshal(protocol.TextContent{
|
||||||
|
Type: "text",
|
||||||
|
Text: "Say hi to " + params.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &protocol.GetPromptResult{
|
||||||
|
Description: "Code review prompt",
|
||||||
|
Messages: []protocol.PromptMessage{
|
||||||
|
// TODO: move 'Content' to the protocol package.
|
||||||
|
{Role: "user", Content: json.RawMessage(content)},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
|
@ -33,6 +53,7 @@ func main() {
|
||||||
server.AddTools(mcp.MakeTool("greet", "say hi", SayHi, mcp.Input(
|
server.AddTools(mcp.MakeTool("greet", "say hi", SayHi, mcp.Input(
|
||||||
mcp.Property("name", mcp.Description("the name to say hi to")),
|
mcp.Property("name", mcp.Description("the name to say hi to")),
|
||||||
)))
|
)))
|
||||||
|
server.AddPrompts(mcp.MakePrompt("greet", "", PromptHi))
|
||||||
|
|
||||||
if *httpAddr != "" {
|
if *httpAddr != "" {
|
||||||
handler := mcp.NewSSEHandler(func(*http.Request) *mcp.Server {
|
handler := mcp.NewSSEHandler(func(*http.Request) *mcp.Server {
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ func parseField(f reflect.StructField) (name string, required, include bool) {
|
||||||
}
|
}
|
||||||
name = props[0]
|
name = props[0]
|
||||||
}
|
}
|
||||||
|
// TODO: support 'omitzero' as well.
|
||||||
required = !slices.Contains(props[1:], "omitempty")
|
required = !slices.Contains(props[1:], "omitempty")
|
||||||
}
|
}
|
||||||
return name, required, true
|
return name, required, true
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,11 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
"io"
|
"io"
|
||||||
"iter"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -28,6 +26,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/modelcontextprotocol/go-sdk/internal/jsonschema"
|
"github.com/modelcontextprotocol/go-sdk/internal/jsonschema"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var schemaFile = flag.String("schema_file", "", "if set, use this file as the persistent schema file")
|
var schemaFile = flag.String("schema_file", "", "if set, use this file as the persistent schema file")
|
||||||
|
|
@ -54,31 +53,36 @@ var declarations = config{
|
||||||
"CallToolRequest": {
|
"CallToolRequest": {
|
||||||
Fields: config{"Params": {Name: "CallToolParams"}},
|
Fields: config{"Params": {Name: "CallToolParams"}},
|
||||||
},
|
},
|
||||||
"CallToolResult": {
|
"CallToolResult": {Name: "CallToolResult"},
|
||||||
Name: "CallToolResult",
|
|
||||||
},
|
|
||||||
"CancelledNotification": {
|
"CancelledNotification": {
|
||||||
Fields: config{"Params": {Name: "CancelledParams"}},
|
Fields: config{"Params": {Name: "CancelledParams"}},
|
||||||
},
|
},
|
||||||
"ClientCapabilities": {Name: "ClientCapabilities"},
|
"ClientCapabilities": {Name: "ClientCapabilities"},
|
||||||
"Implementation": {Name: "Implementation"},
|
"GetPromptRequest": {
|
||||||
|
Fields: config{"Params": {Name: "GetPromptParams"}},
|
||||||
|
},
|
||||||
|
"GetPromptResult": {Name: "GetPromptResult"},
|
||||||
|
"Implementation": {Name: "Implementation"},
|
||||||
"InitializeRequest": {
|
"InitializeRequest": {
|
||||||
Fields: config{"Params": {Name: "InitializeParams"}},
|
Fields: config{"Params": {Name: "InitializeParams"}},
|
||||||
},
|
},
|
||||||
"InitializeResult": {
|
"InitializeResult": {Name: "InitializeResult"},
|
||||||
Name: "InitializeResult",
|
|
||||||
},
|
|
||||||
"InitializedNotification": {
|
"InitializedNotification": {
|
||||||
Fields: config{"Params": {Name: "InitializedParams"}},
|
Fields: config{"Params": {Name: "InitializedParams"}},
|
||||||
},
|
},
|
||||||
|
"ListPromptsRequest": {
|
||||||
|
Fields: config{"Params": {Name: "ListPromptsParams"}},
|
||||||
|
},
|
||||||
|
"ListPromptsResult": {Name: "ListPromptsResult"},
|
||||||
"ListToolsRequest": {
|
"ListToolsRequest": {
|
||||||
Fields: config{"Params": {Name: "ListToolsParams"}},
|
Fields: config{"Params": {Name: "ListToolsParams"}},
|
||||||
},
|
},
|
||||||
"ListToolsResult": {
|
"ListToolsResult": {Name: "ListToolsResult"},
|
||||||
Name: "ListToolsResult",
|
"Prompt": {Name: "Prompt"},
|
||||||
},
|
"PromptMessage": {Name: "PromptMessage"},
|
||||||
"RequestId": {Substitute: "any"}, // null|number|string
|
"PromptArgument": {Name: "PromptArgument"},
|
||||||
"Role": {Name: "Role"},
|
"RequestId": {Substitute: "any"}, // null|number|string
|
||||||
|
"Role": {Name: "Role"},
|
||||||
"ServerCapabilities": {
|
"ServerCapabilities": {
|
||||||
Name: "ServerCapabilities",
|
Name: "ServerCapabilities",
|
||||||
Fields: config{
|
Fields: config{
|
||||||
|
|
@ -92,9 +96,7 @@ var declarations = config{
|
||||||
Name: "Tool",
|
Name: "Tool",
|
||||||
Fields: config{"InputSchema": {Substitute: "*jsonschema.Schema"}},
|
Fields: config{"InputSchema": {Substitute: "*jsonschema.Schema"}},
|
||||||
},
|
},
|
||||||
"ToolAnnotations": {
|
"ToolAnnotations": {Name: "ToolAnnotations"},
|
||||||
Name: "ToolAnnotations",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -114,7 +116,7 @@ func main() {
|
||||||
// writing types, we collect definitions and concatenate them later. This
|
// writing types, we collect definitions and concatenate them later. This
|
||||||
// also allows us to sort.
|
// also allows us to sort.
|
||||||
named := make(map[string]*bytes.Buffer)
|
named := make(map[string]*bytes.Buffer)
|
||||||
for name, def := range sorted(schema.Definitions) {
|
for name, def := range util.Sorted(schema.Definitions) {
|
||||||
config := declarations[name]
|
config := declarations[name]
|
||||||
if config == nil {
|
if config == nil {
|
||||||
continue
|
continue
|
||||||
|
|
@ -142,7 +144,7 @@ import (
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// Write out types.
|
// Write out types.
|
||||||
for _, b := range sorted(named) {
|
for _, b := range util.Sorted(named) {
|
||||||
fmt.Fprintln(buf)
|
fmt.Fprintln(buf)
|
||||||
fmt.Fprint(buf, b.String())
|
fmt.Fprint(buf, b.String())
|
||||||
}
|
}
|
||||||
|
|
@ -242,8 +244,8 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
|
||||||
// unmarshal them into a map[string]any, or delay unmarshalling with
|
// unmarshal them into a map[string]any, or delay unmarshalling with
|
||||||
// json.RawMessage. For now, use json.RawMessage as it defers the choice.
|
// json.RawMessage. For now, use json.RawMessage as it defers the choice.
|
||||||
if def.Type == "object" && canHaveAdditionalProperties(def) {
|
if def.Type == "object" && canHaveAdditionalProperties(def) {
|
||||||
w.Write([]byte("json.RawMessage"))
|
w.Write([]byte("map[string]"))
|
||||||
return nil
|
return writeType(w, nil, def.AdditionalProperties, named)
|
||||||
}
|
}
|
||||||
|
|
||||||
if def.Type == "" {
|
if def.Type == "" {
|
||||||
|
|
@ -269,7 +271,7 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
|
||||||
|
|
||||||
case "object":
|
case "object":
|
||||||
fmt.Fprintf(w, "struct {\n")
|
fmt.Fprintf(w, "struct {\n")
|
||||||
for name, fieldDef := range sorted(def.Properties) {
|
for name, fieldDef := range util.Sorted(def.Properties) {
|
||||||
if fieldDef.Description != "" {
|
if fieldDef.Description != "" {
|
||||||
fmt.Fprintf(w, "%s\n", toComment(fieldDef.Description))
|
fmt.Fprintf(w, "%s\n", toComment(fieldDef.Description))
|
||||||
}
|
}
|
||||||
|
|
@ -385,28 +387,3 @@ func assert(cond bool, msg string) {
|
||||||
panic(msg)
|
panic(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers below are copied from gopls' moremaps package.
|
|
||||||
|
|
||||||
// sorted returns an iterator over the entries of m in key order.
|
|
||||||
func sorted[M ~map[K]V, K cmp.Ordered, V any](m M) iter.Seq2[K, V] {
|
|
||||||
// TODO(adonovan): use maps.Sorted if proposal #68598 is accepted.
|
|
||||||
return func(yield func(K, V) bool) {
|
|
||||||
keys := keySlice(m)
|
|
||||||
slices.Sort(keys)
|
|
||||||
for _, k := range keys {
|
|
||||||
if !yield(k, m[k]) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// keySlice returns the keys of the map M, like slices.Collect(maps.Keys(m)).
|
|
||||||
func keySlice[M ~map[K]V, K comparable, V any](m M) []K {
|
|
||||||
r := make([]K, 0, len(m))
|
|
||||||
for k := range m {
|
|
||||||
r = append(r, k)
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ type Annotations struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type CallToolParams struct {
|
type CallToolParams struct {
|
||||||
Arguments json.RawMessage `json:"arguments,omitempty"`
|
Arguments map[string]json.RawMessage `json:"arguments,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The server's response to a tool call.
|
// The server's response to a tool call.
|
||||||
|
|
@ -46,8 +46,8 @@ type CallToolParams struct {
|
||||||
type CallToolResult struct {
|
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 json.RawMessage `json:"_meta,omitempty"`
|
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
|
||||||
Content []json.RawMessage `json:"content"`
|
Content []json.RawMessage `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).
|
||||||
|
|
@ -70,14 +70,31 @@ type CancelledParams struct {
|
||||||
// additional capabilities.
|
// additional capabilities.
|
||||||
type ClientCapabilities struct {
|
type ClientCapabilities struct {
|
||||||
// Experimental, non-standard capabilities that the client supports.
|
// Experimental, non-standard capabilities that the client supports.
|
||||||
Experimental json.RawMessage `json:"experimental,omitempty"`
|
Experimental map[string]map[string]json.RawMessage `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"`
|
||||||
// Present if the client supports sampling from an LLM.
|
// Present if the client supports sampling from an LLM.
|
||||||
Sampling json.RawMessage `json:"sampling,omitempty"`
|
Sampling map[string]json.RawMessage `json:"sampling,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPromptParams struct {
|
||||||
|
// Arguments to use for templating the prompt.
|
||||||
|
Arguments map[string]string `json:"arguments,omitempty"`
|
||||||
|
// The name of the prompt or prompt template.
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server's response to a prompts/get request from the client.
|
||||||
|
type GetPromptResult 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"`
|
||||||
|
// An optional description for the prompt.
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Messages []PromptMessage `json:"messages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Describes the name and version of an MCP implementation.
|
// Describes the name and version of an MCP implementation.
|
||||||
|
|
@ -99,8 +116,8 @@ type InitializeParams struct {
|
||||||
type InitializeResult struct {
|
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 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
|
||||||
|
|
@ -114,7 +131,24 @@ type InitializeResult struct {
|
||||||
ServerInfo Implementation `json:"serverInfo"`
|
ServerInfo Implementation `json:"serverInfo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InitializedParams json.RawMessage
|
type InitializedParams map[string]json.RawMessage
|
||||||
|
|
||||||
|
type ListPromptsParams struct {
|
||||||
|
// An opaque token representing the current pagination position. If provided,
|
||||||
|
// the server should return results starting after this cursor.
|
||||||
|
Cursor string `json:"cursor,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server's response to a prompts/list request from the client.
|
||||||
|
type ListPromptsResult 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"`
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
type ListToolsParams struct {
|
type ListToolsParams struct {
|
||||||
// An opaque token representing the current pagination position. If provided,
|
// An opaque token representing the current pagination position. If provided,
|
||||||
|
|
@ -126,19 +160,48 @@ type ListToolsParams struct {
|
||||||
type ListToolsResult struct {
|
type ListToolsResult 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 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.
|
||||||
|
type Prompt struct {
|
||||||
|
// A list of arguments to use for templating the prompt.
|
||||||
|
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.
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describes an argument that a prompt can accept.
|
||||||
|
type PromptArgument struct {
|
||||||
|
// A human-readable description of the argument.
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
// The name of the argument.
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Whether this argument must be provided.
|
||||||
|
Required bool `json:"required,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Present if the server offers any prompt templates.
|
// Present if the server offers any prompt templates.
|
||||||
type PromptCapabilities struct {
|
type PromptCapabilities struct {
|
||||||
// Whether this server supports notifications for changes to the prompt list.
|
// Whether this server supports notifications for changes to the prompt list.
|
||||||
ListChanged bool `json:"listChanged,omitempty"`
|
ListChanged bool `json:"listChanged,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Describes a message returned as part of a prompt.
|
||||||
|
//
|
||||||
|
// This is similar to `SamplingMessage`, but also supports the embedding of
|
||||||
|
// resources from the MCP server.
|
||||||
|
type PromptMessage struct {
|
||||||
|
Content json.RawMessage `json:"content"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
// Present if the server offers any resources to read.
|
// Present if the server offers any resources to read.
|
||||||
type ResourceCapabilities struct {
|
type ResourceCapabilities struct {
|
||||||
// Whether this server supports notifications for changes to the resource list.
|
// Whether this server supports notifications for changes to the resource list.
|
||||||
|
|
@ -155,11 +218,11 @@ type Role string
|
||||||
// additional capabilities.
|
// additional capabilities.
|
||||||
type ServerCapabilities struct {
|
type ServerCapabilities struct {
|
||||||
// Present if the server supports argument autocompletion suggestions.
|
// Present if the server supports argument autocompletion suggestions.
|
||||||
Completions json.RawMessage `json:"completions,omitempty"`
|
Completions map[string]json.RawMessage `json:"completions,omitempty"`
|
||||||
// Experimental, non-standard capabilities that the server supports.
|
// Experimental, non-standard capabilities that the server supports.
|
||||||
Experimental json.RawMessage `json:"experimental,omitempty"`
|
Experimental map[string]map[string]json.RawMessage `json:"experimental,omitempty"`
|
||||||
// Present if the server supports sending log messages to the client.
|
// Present if the server supports sending log messages to the client.
|
||||||
Logging json.RawMessage `json:"logging,omitempty"`
|
Logging map[string]json.RawMessage `json:"logging,omitempty"`
|
||||||
// Present if the server offers any prompt templates.
|
// Present if the server offers any prompt templates.
|
||||||
Prompts *PromptCapabilities `json:"prompts,omitempty"`
|
Prompts *PromptCapabilities `json:"prompts,omitempty"`
|
||||||
// Present if the server offers any resources to read.
|
// Present if the server offers any resources to read.
|
||||||
|
|
|
||||||
36
internal/util/util.go
Normal file
36
internal/util/util.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"iter"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helpers below are copied from gopls' moremaps package.
|
||||||
|
|
||||||
|
// sorted returns an iterator over the entries of m in key order.
|
||||||
|
func Sorted[M ~map[K]V, K cmp.Ordered, V any](m M) iter.Seq2[K, V] {
|
||||||
|
// TODO(adonovan): use maps.Sorted if proposal #68598 is accepted.
|
||||||
|
return func(yield func(K, V) bool) {
|
||||||
|
keys := KeySlice(m)
|
||||||
|
slices.Sort(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
if !yield(k, m[k]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keySlice returns the keys of the map M, like slices.Collect(maps.Keys(m)).
|
||||||
|
func KeySlice[M ~map[K]V, K comparable, V any](m M) []K {
|
||||||
|
r := make([]K, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
r = append(r, k)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
@ -150,6 +150,33 @@ func (sc *ServerConnection) Ping(ctx context.Context) error {
|
||||||
return call(ctx, sc.conn, "ping", nil, nil)
|
return call(ctx, sc.conn, "ping", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListPrompts lists prompts that are currently available on the server.
|
||||||
|
func (sc *ServerConnection) ListPrompts(ctx context.Context) ([]protocol.Prompt, error) {
|
||||||
|
var (
|
||||||
|
params = &protocol.ListPromptsParams{}
|
||||||
|
result protocol.ListPromptsResult
|
||||||
|
)
|
||||||
|
if err := call(ctx, sc.conn, "prompts/list", params, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result.Prompts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrompt gets a prompt from the server.
|
||||||
|
func (sc *ServerConnection) GetPrompt(ctx context.Context, name string, args map[string]string) (*protocol.GetPromptResult, error) {
|
||||||
|
var (
|
||||||
|
params = &protocol.GetPromptParams{
|
||||||
|
Name: name,
|
||||||
|
Arguments: args,
|
||||||
|
}
|
||||||
|
result = &protocol.GetPromptResult{}
|
||||||
|
)
|
||||||
|
if err := call(ctx, sc.conn, "prompts/get", params, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListTools lists tools that are currently available on the server.
|
// ListTools lists tools that are currently available on the server.
|
||||||
func (sc *ServerConnection) ListTools(ctx context.Context) ([]protocol.Tool, error) {
|
func (sc *ServerConnection) ListTools(ctx context.Context) ([]protocol.Tool, error) {
|
||||||
var (
|
var (
|
||||||
|
|
@ -164,23 +191,27 @@ func (sc *ServerConnection) ListTools(ctx context.Context) ([]protocol.Tool, err
|
||||||
|
|
||||||
// CallTool calls the tool with the given name and arguments.
|
// CallTool calls the tool with the given name and arguments.
|
||||||
//
|
//
|
||||||
// TODO: make the following true:
|
// TODO(jba): make the following true:
|
||||||
// If the provided arguments do not conform to the schema for the given tool,
|
// If the provided arguments do not conform to the schema for the given tool,
|
||||||
// the call fails.
|
// the call fails.
|
||||||
func (sc *ServerConnection) CallTool(ctx context.Context, name string, args any) (_ []Content, err error) {
|
func (sc *ServerConnection) CallTool(ctx context.Context, name string, args map[string]any) (_ []Content, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("calling tool %q: %w", name, err)
|
err = fmt.Errorf("calling tool %q: %w", name, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
argJSON, err := json.Marshal(args)
|
argsJSON := make(map[string]json.RawMessage)
|
||||||
if err != nil {
|
for name, arg := range args {
|
||||||
return nil, fmt.Errorf("marshaling args: %v", err)
|
argJSON, err := json.Marshal(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling argument %s: %v", name, err)
|
||||||
|
}
|
||||||
|
argsJSON[name] = argJSON
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
params = &protocol.CallToolParams{
|
params = &protocol.CallToolParams{
|
||||||
Name: name,
|
Name: name,
|
||||||
Arguments: argJSON,
|
Arguments: argsJSON,
|
||||||
}
|
}
|
||||||
result protocol.CallToolResult
|
result protocol.CallToolResult
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ func TestCmdTransport(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
got, err := serverConn.CallTool(ctx, "greet", SayHiParams{Name: "user"})
|
got, err := serverConn.CallTool(ctx, "greet", map[string]any{"name": "user"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,12 @@
|
||||||
//
|
//
|
||||||
// # TODO
|
// # TODO
|
||||||
//
|
//
|
||||||
|
// - Support all content types.
|
||||||
// - Support pagination.
|
// - Support pagination.
|
||||||
|
// - Support completion.
|
||||||
|
// - Support oauth.
|
||||||
// - Support all client/server operations.
|
// - Support all client/server operations.
|
||||||
|
// - Pass the client connection in the context.
|
||||||
// - Support streamable HTTP transport.
|
// - Support streamable HTTP transport.
|
||||||
// - Support multiple versions of the spec.
|
// - Support multiple versions of the spec.
|
||||||
// - Implement full JSON schema support, with both client-side and
|
// - Implement full JSON schema support, with both client-side and
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package mcp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
@ -41,9 +42,34 @@ 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(MakeTool("fail", "just fail", func(context.Context, *ClientConnection, struct{}) ([]Content, error) {
|
s.AddTools(
|
||||||
return nil, failure
|
MakeTool("fail", "just fail", func(context.Context, *ClientConnection, struct{}) ([]Content, error) {
|
||||||
}))
|
return nil, failure
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
s.AddPrompts(
|
||||||
|
MakePrompt("code_review", "do a code review", func(_ context.Context, _ *ClientConnection, params struct{ Code string }) (*protocol.GetPromptResult, error) {
|
||||||
|
// TODO(rfindley): clean up this handling of content.
|
||||||
|
content, err := json.Marshal(protocol.TextContent{
|
||||||
|
Type: "text",
|
||||||
|
Text: "Please review the following code: " + params.Code,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &protocol.GetPromptResult{
|
||||||
|
Description: "Code review prompt",
|
||||||
|
Messages: []protocol.PromptMessage{
|
||||||
|
// TODO: move 'Content' to the protocol package.
|
||||||
|
{Role: "user", Content: json.RawMessage(content)},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
MakePrompt("fail", "", func(_ context.Context, _ *ClientConnection, params struct{}) (*protocol.GetPromptResult, error) {
|
||||||
|
return nil, failure
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// Connect the server.
|
// Connect the server.
|
||||||
cc, err := s.Connect(ctx, st, nil)
|
cc, err := s.Connect(ctx, st, nil)
|
||||||
|
|
@ -80,6 +106,34 @@ func TestEndToEnd(t *testing.T) {
|
||||||
t.Fatalf("ping failed: %v", err)
|
t.Fatalf("ping failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gotPrompts, err := sc.ListPrompts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("prompts/list failed: %v", err)
|
||||||
|
}
|
||||||
|
wantPrompts := []protocol.Prompt{
|
||||||
|
{
|
||||||
|
Name: "code_review",
|
||||||
|
Description: "do a code review",
|
||||||
|
Arguments: []protocol.PromptArgument{{Name: "Code", Required: true}},
|
||||||
|
},
|
||||||
|
{Name: "fail"},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(wantPrompts, gotPrompts); diff != "" {
|
||||||
|
t.Fatalf("prompts/list mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotReview, err := sc.GetPrompt(ctx, "code_review", map[string]string{"Code": "1+1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// TODO: assert on the full review, once content is easier to create.
|
||||||
|
if got, want := gotReview.Description, "Code review prompt"; got != want {
|
||||||
|
t.Errorf("prompts/get 'code_review': got description %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if _, err := sc.GetPrompt(ctx, "fail", map[string]string{}); err == nil || !strings.Contains(err.Error(), failure.Error()) {
|
||||||
|
t.Errorf("fail returned unexpected error: got %v, want containing %v", err, failure)
|
||||||
|
}
|
||||||
|
|
||||||
gotTools, err := sc.ListTools(ctx)
|
gotTools, err := sc.ListTools(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("tools/list failed: %v", err)
|
t.Errorf("tools/list failed: %v", err)
|
||||||
|
|
@ -107,7 +161,7 @@ func TestEndToEnd(t *testing.T) {
|
||||||
t.Fatalf("tools/list mismatch (-want +got):\n%s", diff)
|
t.Fatalf("tools/list mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotHi, err := sc.CallTool(ctx, "greet", hiParams{"user"})
|
gotHi, err := sc.CallTool(ctx, "greet", map[string]any{"name": "user"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +170,7 @@ func TestEndToEnd(t *testing.T) {
|
||||||
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
|
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := sc.CallTool(ctx, "fail", struct{}{}); err == nil || !strings.Contains(err.Error(), failure.Error()) {
|
if _, err := sc.CallTool(ctx, "fail", map[string]any{}); err == nil || !strings.Contains(err.Error(), failure.Error()) {
|
||||||
t.Errorf("fail returned unexpected error: got %v, want containing %v", err, failure)
|
t.Errorf("fail returned unexpected error: got %v, want containing %v", err, failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,12 +229,12 @@ func TestServerClosing(t *testing.T) {
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
if _, err := sc.CallTool(ctx, "greet", hiParams{"user"}); err != nil {
|
if _, err := sc.CallTool(ctx, "greet", map[string]any{"name": "user"}); err != nil {
|
||||||
t.Fatalf("after connecting: %v", err)
|
t.Fatalf("after connecting: %v", err)
|
||||||
}
|
}
|
||||||
cc.Close()
|
cc.Close()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if _, err := sc.CallTool(ctx, "greet", hiParams{"user"}); !errors.Is(err, ErrConnectionClosed) {
|
if _, err := sc.CallTool(ctx, "greet", map[string]any{"name": "user"}); !errors.Is(err, ErrConnectionClosed) {
|
||||||
t.Errorf("after disconnection, got error %v, want EOF", err)
|
t.Errorf("after disconnection, got error %v, want EOF", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +298,7 @@ func TestCancellation(t *testing.T) {
|
||||||
defer sc.Close()
|
defer sc.Close()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
go sc.CallTool(ctx, "slow", struct{}{})
|
go sc.CallTool(ctx, "slow", map[string]any{})
|
||||||
<-start
|
<-start
|
||||||
cancel()
|
cancel()
|
||||||
select {
|
select {
|
||||||
|
|
|
||||||
122
mcp/prompt.go
Normal file
122
mcp/prompt.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/internal/jsonschema"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/internal/protocol"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A PromptHandler handles a call to prompts/get.
|
||||||
|
type PromptHandler func(context.Context, *ClientConnection, map[string]string) (*protocol.GetPromptResult, error)
|
||||||
|
|
||||||
|
// A Prompt is a prompt definition bound to a prompt handler.
|
||||||
|
type Prompt struct {
|
||||||
|
Definition protocol.Prompt
|
||||||
|
Handler PromptHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakePrompt is a helper to use reflection to create a prompt for the given
|
||||||
|
// handler.
|
||||||
|
//
|
||||||
|
// The arguments for the prompt are extracted from the request type for the
|
||||||
|
// handler. The handler request type must be a struct consisting only of fields
|
||||||
|
// of type string or *string. The argument names for the resulting prompt
|
||||||
|
// definition correspond to the JSON names of the request fields, and any
|
||||||
|
// fields that are not marked "omitempty" are considered required.
|
||||||
|
func MakePrompt[TReq any](name, description string, handler func(context.Context, *ClientConnection, TReq) (*protocol.GetPromptResult, error), opts ...PromptOption) *Prompt {
|
||||||
|
schema, err := jsonschema.For[TReq]()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if schema.Type != "object" || !reflect.DeepEqual(schema.AdditionalProperties, &jsonschema.Schema{Not: &jsonschema.Schema{}}) {
|
||||||
|
panic(fmt.Sprintf("handler request type must be a struct"))
|
||||||
|
}
|
||||||
|
prompt := &Prompt{
|
||||||
|
Definition: protocol.Prompt{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
required := make(map[string]bool)
|
||||||
|
for _, p := range schema.Required {
|
||||||
|
required[p] = true
|
||||||
|
}
|
||||||
|
for name, prop := range util.Sorted(schema.Properties) {
|
||||||
|
if prop.Type != "string" {
|
||||||
|
panic(fmt.Sprintf("handler type must consist only of string fields"))
|
||||||
|
}
|
||||||
|
prompt.Definition.Arguments = append(prompt.Definition.Arguments, protocol.PromptArgument{
|
||||||
|
Name: name,
|
||||||
|
Description: prop.Description,
|
||||||
|
Required: required[name],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
prompt.Handler = func(ctx context.Context, cc *ClientConnection, args map[string]string) (*protocol.GetPromptResult, error) {
|
||||||
|
// For simplicity, just marshal and unmarshal the arguments.
|
||||||
|
// This could be avoided in the future.
|
||||||
|
rawArgs, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var v TReq
|
||||||
|
if err := unmarshalSchema(rawArgs, schema, &v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return handler(ctx, cc, v)
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.set(prompt)
|
||||||
|
}
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// A PromptOption configures the behavior of a Prompt.
|
||||||
|
type PromptOption interface {
|
||||||
|
set(*Prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptSetter func(*Prompt)
|
||||||
|
|
||||||
|
func (s promptSetter) set(p *Prompt) { s(p) }
|
||||||
|
|
||||||
|
// Argument configures the 'schema' of a prompt argument.
|
||||||
|
// If the argument does not exist, it is added.
|
||||||
|
//
|
||||||
|
// Since prompt arguments are not a full JSON schema, Argument only accepts
|
||||||
|
// Required and Description, and panics when encountering any other option.
|
||||||
|
func Argument(name string, opts ...SchemaOption) PromptOption {
|
||||||
|
return promptSetter(func(p *Prompt) {
|
||||||
|
i := slices.IndexFunc(p.Definition.Arguments, func(arg protocol.PromptArgument) bool {
|
||||||
|
return arg.Name == name
|
||||||
|
})
|
||||||
|
var arg protocol.PromptArgument
|
||||||
|
if i < 0 {
|
||||||
|
i = len(p.Definition.Arguments)
|
||||||
|
arg = protocol.PromptArgument{Name: name}
|
||||||
|
p.Definition.Arguments = append(p.Definition.Arguments, arg)
|
||||||
|
} else {
|
||||||
|
arg = p.Definition.Arguments[i]
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
switch v := opt.(type) {
|
||||||
|
case required:
|
||||||
|
arg.Required = bool(v)
|
||||||
|
case description:
|
||||||
|
arg.Description = string(v)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported prompt argument schema option %T", opt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Definition.Arguments[i] = arg
|
||||||
|
})
|
||||||
|
}
|
||||||
54
mcp/prompt_test.go
Normal file
54
mcp/prompt_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mcp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/internal/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testPromptHandler is used for type inference in TestMakePrompt.
|
||||||
|
func testPromptHandler[T any](context.Context, *mcp.ClientConnection, T) (*protocol.GetPromptResult, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakePrompt(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
prompt *mcp.Prompt
|
||||||
|
want []protocol.PromptArgument
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
mcp.MakePrompt("empty", "", testPromptHandler[struct{}]),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mcp.MakePrompt("add_arg", "", testPromptHandler[struct{}], mcp.Argument("x")),
|
||||||
|
[]protocol.PromptArgument{{Name: "x"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mcp.MakePrompt("combo", "", testPromptHandler[struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
State string
|
||||||
|
}],
|
||||||
|
mcp.Argument("name", mcp.Description("the person's name")),
|
||||||
|
mcp.Argument("State", mcp.Required(false))),
|
||||||
|
[]protocol.PromptArgument{
|
||||||
|
{Name: "State"},
|
||||||
|
{Name: "country"},
|
||||||
|
{Name: "name", Required: true, Description: "the person's name"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
if diff := cmp.Diff(test.want, test.prompt.Definition.Arguments); diff != "" {
|
||||||
|
t.Errorf("MakePrompt(%v) mismatch (-want +got):\n%s", test.prompt.Definition.Name, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ type Server struct {
|
||||||
opts ServerOptions
|
opts ServerOptions
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
prompts []*Prompt
|
||||||
tools []*Tool
|
tools []*Tool
|
||||||
clients []*ClientConnection
|
clients []*ClientConnection
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +54,15 @@ func NewServer(name, version string, opts *ServerOptions) *Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddPrompts adds the given prompts to the server.
|
||||||
|
//
|
||||||
|
// TODO(rfindley): notify connected clients of any changes.
|
||||||
|
func (s *Server) AddPrompts(prompts ...*Prompt) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.prompts = append(s.prompts, prompts...)
|
||||||
|
}
|
||||||
|
|
||||||
// AddTools adds the given tools to the server.
|
// AddTools adds the given tools to the server.
|
||||||
//
|
//
|
||||||
// TODO(rfindley): notify connected clients of any changes.
|
// TODO(rfindley): notify connected clients of any changes.
|
||||||
|
|
@ -71,6 +81,33 @@ func (s *Server) Clients() iter.Seq[*ClientConnection] {
|
||||||
return slices.Values(clients)
|
return slices.Values(clients)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) listPrompts(_ context.Context, _ *ClientConnection, params *protocol.ListPromptsParams) (*protocol.ListPromptsResult, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
res := new(protocol.ListPromptsResult)
|
||||||
|
for _, p := range s.prompts {
|
||||||
|
res.Prompts = append(res.Prompts, p.Definition)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getPrompt(ctx context.Context, cc *ClientConnection, params *protocol.GetPromptParams) (*protocol.GetPromptResult, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
var prompt *Prompt
|
||||||
|
if i := slices.IndexFunc(s.prompts, func(t *Prompt) bool {
|
||||||
|
return t.Definition.Name == params.Name
|
||||||
|
}); i >= 0 {
|
||||||
|
prompt = s.prompts[i]
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if prompt == nil {
|
||||||
|
return nil, fmt.Errorf("%s: unknown prompt %q", jsonrpc2.ErrInvalidParams, params.Name)
|
||||||
|
}
|
||||||
|
return prompt.Handler(ctx, cc, params.Arguments)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) listTools(_ context.Context, _ *ClientConnection, params *protocol.ListToolsParams) (*protocol.ListToolsResult, error) {
|
func (s *Server) listTools(_ context.Context, _ *ClientConnection, params *protocol.ListToolsParams) (*protocol.ListToolsResult, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
@ -185,6 +222,12 @@ func (cc *ClientConnection) handle(ctx context.Context, req *jsonrpc2.Request) (
|
||||||
// The spec says that 'ping' expects an empty object result.
|
// The spec says that 'ping' expects an empty object result.
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
|
|
||||||
|
case "prompts/list":
|
||||||
|
return dispatch(ctx, cc, req, cc.server.listPrompts)
|
||||||
|
|
||||||
|
case "prompts/get":
|
||||||
|
return dispatch(ctx, cc, req, cc.server.getPrompt)
|
||||||
|
|
||||||
case "tools/list":
|
case "tools/list":
|
||||||
return dispatch(ctx, cc, req, cc.server.listTools)
|
return dispatch(ctx, cc, req, cc.server.listTools)
|
||||||
|
|
||||||
|
|
@ -216,8 +259,11 @@ func (cc *ClientConnection) initialize(ctx context.Context, _ *ClientConnection,
|
||||||
// TODO(rfindley): support multiple protocol versions.
|
// TODO(rfindley): support multiple protocol versions.
|
||||||
ProtocolVersion: "2024-11-05",
|
ProtocolVersion: "2024-11-05",
|
||||||
Capabilities: protocol.ServerCapabilities{
|
Capabilities: protocol.ServerCapabilities{
|
||||||
|
Prompts: &protocol.PromptCapabilities{
|
||||||
|
ListChanged: false, // not yet supported
|
||||||
|
},
|
||||||
Tools: &protocol.ToolCapabilities{
|
Tools: &protocol.ToolCapabilities{
|
||||||
ListChanged: true,
|
ListChanged: false, // not yet supported
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Instructions: cc.server.opts.Instructions,
|
Instructions: cc.server.opts.Instructions,
|
||||||
|
|
@ -249,5 +295,11 @@ func dispatch[TConn, TParams, TResult any](ctx context.Context, conn TConn, req
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return f(ctx, conn, params)
|
// Important: avoid returning a typed nil, as it can't be handled by the
|
||||||
|
// jsonrpc2 package.
|
||||||
|
res, err := f(ctx, conn, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ func ExampleServer() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := serverConnection.CallTool(ctx, "greet", SayHiParams{Name: "user"})
|
content, err := serverConnection.CallTool(ctx, "greet", map[string]any{"name": "user"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ func ExampleSSEHandler() {
|
||||||
}
|
}
|
||||||
defer serverConn.Close()
|
defer serverConn.Close()
|
||||||
|
|
||||||
content, err := serverConn.CallTool(ctx, "add", AddParams{1, 2})
|
content, err := serverConn.CallTool(ctx, "add", map[string]any{"x": 1, "y": 2})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func TestSSEServer(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
cc := <-clients
|
cc := <-clients
|
||||||
gotHi, err := sc.CallTool(ctx, "greet", hiParams{"user"})
|
gotHi, err := sc.CallTool(ctx, "greet", map[string]any{"name": "user"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
mcp/tool.go
38
mcp/tool.go
|
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// A ToolHandler handles a call to tools/call.
|
// A ToolHandler handles a call to tools/call.
|
||||||
type ToolHandler func(context.Context, *ClientConnection, json.RawMessage) (*protocol.CallToolResult, error)
|
type ToolHandler func(context.Context, *ClientConnection, map[string]json.RawMessage) (*protocol.CallToolResult, error)
|
||||||
|
|
||||||
// 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 Tool struct {
|
type Tool struct {
|
||||||
|
|
@ -30,20 +30,30 @@ type Tool struct {
|
||||||
// handler, and used to unmmarshal and validate requests to the handler. This
|
// handler, and used to unmmarshal and validate requests to the handler. This
|
||||||
// schema may be customized using the [Input] option.
|
// schema may be customized using the [Input] option.
|
||||||
//
|
//
|
||||||
// It is the caller's responsibility that the handler request type can produce
|
// The handler request type must translate to a valid schema, as documented by
|
||||||
// a valid schema, as documented by [jsonschema.ForType]; otherwise, MakeTool
|
// [jsonschema.ForType]; otherwise, MakeTool panics.
|
||||||
// panics.
|
//
|
||||||
|
// TODO: just have the handler return a CallToolResult: returning []Content is
|
||||||
|
// going to be inconsistent with other server features.
|
||||||
func MakeTool[TReq any](name, description string, handler func(context.Context, *ClientConnection, TReq) ([]Content, error), opts ...ToolOption) *Tool {
|
func MakeTool[TReq any](name, description string, handler func(context.Context, *ClientConnection, TReq) ([]Content, error), opts ...ToolOption) *Tool {
|
||||||
schema, err := jsonschema.For[TReq]()
|
schema, err := jsonschema.For[TReq]()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
wrapped := func(ctx context.Context, cc *ClientConnection, args json.RawMessage) (*protocol.CallToolResult, error) {
|
wrapped := func(ctx context.Context, cc *ClientConnection, args map[string]json.RawMessage) (*protocol.CallToolResult, error) {
|
||||||
|
// For simplicity, just marshal and unmarshal the arguments.
|
||||||
|
// This could be avoided in the future.
|
||||||
|
rawArgs, err := json.Marshal(args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var v TReq
|
var v TReq
|
||||||
if err := unmarshalSchema(args, schema, &v); err != nil {
|
if err := unmarshalSchema(rawArgs, schema, &v); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
content, err := handler(ctx, cc, v)
|
content, err := handler(ctx, cc, v)
|
||||||
|
// TODO: investigate why server errors are embedded in this strange way,
|
||||||
|
// rather than returned as jsonrpc2 server errors.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &protocol.CallToolResult{
|
return &protocol.CallToolResult{
|
||||||
Content: marshalContent([]Content{TextContent{Text: err.Error()}}),
|
Content: marshalContent([]Content{TextContent{Text: err.Error()}}),
|
||||||
|
|
@ -140,6 +150,8 @@ func Required(v bool) SchemaOption {
|
||||||
return required(v)
|
return required(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// required must be a distinguished type as it needs special handling to mutate
|
||||||
|
// the parent schema, and to mutate prompt arguments.
|
||||||
type required bool
|
type required bool
|
||||||
|
|
||||||
func (required) set(s *jsonschema.Schema) {
|
func (required) set(s *jsonschema.Schema) {
|
||||||
|
|
@ -154,10 +166,16 @@ func Enum(values ...any) SchemaOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Description sets the provided schema description.
|
// Description sets the provided schema description.
|
||||||
func Description(description string) SchemaOption {
|
func Description(desc string) SchemaOption {
|
||||||
return schemaSetter(func(schema *jsonschema.Schema) {
|
return description(desc)
|
||||||
schema.Description = description
|
}
|
||||||
})
|
|
||||||
|
// description must be a distinguished type so that it can be handled by prompt
|
||||||
|
// options.
|
||||||
|
type description string
|
||||||
|
|
||||||
|
func (d description) set(s *jsonschema.Schema) {
|
||||||
|
s.Description = string(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schema overrides the inferred schema with a shallow copy of the given
|
// Schema overrides the inferred schema with a shallow copy of the given
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import (
|
||||||
"github.com/modelcontextprotocol/go-sdk/internal/jsonschema"
|
"github.com/modelcontextprotocol/go-sdk/internal/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testHandler is used for type inference in TestMakeTool.
|
// testToolHandler is used for type inference in TestMakeTool.
|
||||||
func testHandler[T any](context.Context, *mcp.ClientConnection, T) ([]mcp.Content, error) {
|
func testToolHandler[T any](context.Context, *mcp.ClientConnection, T) ([]mcp.Content, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ func TestMakeTool(t *testing.T) {
|
||||||
want *jsonschema.Schema
|
want *jsonschema.Schema
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
mcp.MakeTool("basic", "", testHandler[struct {
|
mcp.MakeTool("basic", "", testToolHandler[struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}]),
|
}]),
|
||||||
&jsonschema.Schema{
|
&jsonschema.Schema{
|
||||||
|
|
@ -37,7 +37,7 @@ func TestMakeTool(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mcp.MakeTool("enum", "", testHandler[struct{ Name string }], mcp.Input(
|
mcp.MakeTool("enum", "", testToolHandler[struct{ Name string }], mcp.Input(
|
||||||
mcp.Property("Name", mcp.Enum("x", "y", "z")),
|
mcp.Property("Name", mcp.Enum("x", "y", "z")),
|
||||||
)),
|
)),
|
||||||
&jsonschema.Schema{
|
&jsonschema.Schema{
|
||||||
|
|
@ -50,7 +50,7 @@ func TestMakeTool(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mcp.MakeTool("required", "", testHandler[struct {
|
mcp.MakeTool("required", "", testToolHandler[struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
X int `json:"x,omitempty"`
|
X int `json:"x,omitempty"`
|
||||||
|
|
@ -70,7 +70,7 @@ func TestMakeTool(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mcp.MakeTool("set_schema", "", testHandler[struct {
|
mcp.MakeTool("set_schema", "", testToolHandler[struct {
|
||||||
X int `json:"x,omitempty"`
|
X int `json:"x,omitempty"`
|
||||||
Y int `json:"y,omitempty"`
|
Y int `json:"y,omitempty"`
|
||||||
}], mcp.Input(
|
}], mcp.Input(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue