go-sdk/mcp/prompt.go
Rob Findley 347a1a0078 internal/mcp: rename ClientConnection to ServerConnection
It's too confusing that you send server requests through the Client, and
send client requests through the ClientConnection. Rename
ClientConnection to ServerConnection, so that the types on the
server side of the control diagram start with 'Server'.

Change-Id: Idfd27af464a9296a2341cd23b2075cd9d0eb6b45
Reviewed-on: https://go-review.googlesource.com/c/tools/+/670538
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Robert Findley <rfindley@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
2025-05-07 10:17:38 -07:00

122 lines
3.8 KiB
Go

// 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/util"
"github.com/modelcontextprotocol/go-sdk/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp/protocol"
)
// A PromptHandler handles a call to prompts/get.
type PromptHandler func(context.Context, *ServerConnection, 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
}
// NewPrompt 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 NewPrompt[TReq any](name, description string, handler func(context.Context, *ServerConnection, 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 *ServerConnection, 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
})
}