In accordance with the updated design, merge the protocol package into the mcp package. To do so, rename protocol.Content to WireContent and protocol.ResourceContents to WireResource. Also, remove some stray logging in readResource. Change-Id: I76f61ce8d67dd3ac520ef2e1959ade0a0785a480 Reviewed-on: https://go-review.googlesource.com/c/tools/+/672375 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
187 lines
5.4 KiB
Go
187 lines
5.4 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"
|
|
"slices"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/internal/util"
|
|
"github.com/modelcontextprotocol/go-sdk/jsonschema"
|
|
)
|
|
|
|
// A ToolHandler handles a call to tools/call.
|
|
type ToolHandler func(context.Context, *ServerConnection, map[string]json.RawMessage) (*CallToolResult, error)
|
|
|
|
// A Tool is a tool definition that is bound to a tool handler.
|
|
type ServerTool struct {
|
|
Definition Tool
|
|
Handler ToolHandler
|
|
}
|
|
|
|
// NewTool is a helper to make a tool using reflection on the given handler.
|
|
//
|
|
// If provided, variadic [ToolOption] values may be used to customize the tool.
|
|
//
|
|
// The input schema for the tool is extracted from the request type for the
|
|
// handler, and used to unmmarshal and validate requests to the handler. This
|
|
// schema may be customized using the [Input] option.
|
|
//
|
|
// The handler request type must translate to a valid schema, as documented by
|
|
// [jsonschema.ForType]; otherwise, NewTool panics.
|
|
//
|
|
// 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 {
|
|
schema, err := jsonschema.For[TReq]()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
wrapped := func(ctx context.Context, cc *ServerConnection, args map[string]json.RawMessage) (*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
|
|
if err := unmarshalSchema(rawArgs, schema, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
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 {
|
|
return &CallToolResult{
|
|
Content: []WireContent{TextContent{Text: err.Error()}.ToWire()},
|
|
IsError: true,
|
|
}, nil
|
|
}
|
|
res := &CallToolResult{
|
|
Content: util.Apply(content, Content.ToWire),
|
|
}
|
|
return res, nil
|
|
}
|
|
t := &ServerTool{
|
|
Definition: Tool{
|
|
Name: name,
|
|
Description: description,
|
|
InputSchema: schema,
|
|
},
|
|
Handler: wrapped,
|
|
}
|
|
for _, opt := range opts {
|
|
opt.set(t)
|
|
}
|
|
return t
|
|
}
|
|
|
|
// unmarshalSchema unmarshals data into v and validates the result according to
|
|
// the given schema.
|
|
func unmarshalSchema(data json.RawMessage, _ *jsonschema.Schema, v any) error {
|
|
// TODO: use reflection to create the struct type to unmarshal into.
|
|
// Separate validation from assignment.
|
|
return json.Unmarshal(data, v)
|
|
}
|
|
|
|
// A ToolOption configures the behavior of a Tool.
|
|
type ToolOption interface {
|
|
set(*ServerTool)
|
|
}
|
|
|
|
type toolSetter func(*ServerTool)
|
|
|
|
func (s toolSetter) set(t *ServerTool) { s(t) }
|
|
|
|
// Input applies the provided [SchemaOption] configuration to the tool's input
|
|
// schema.
|
|
func Input(opts ...SchemaOption) ToolOption {
|
|
return toolSetter(func(t *ServerTool) {
|
|
for _, opt := range opts {
|
|
opt.set(t.Definition.InputSchema)
|
|
}
|
|
})
|
|
}
|
|
|
|
// A SchemaOption configures a jsonschema.Schema.
|
|
type SchemaOption interface {
|
|
set(s *jsonschema.Schema)
|
|
}
|
|
|
|
type schemaSetter func(*jsonschema.Schema)
|
|
|
|
func (s schemaSetter) set(schema *jsonschema.Schema) { s(schema) }
|
|
|
|
// Property configures the schema for the property of the given name.
|
|
// If there is no such property in the schema, it is created.
|
|
func Property(name string, opts ...SchemaOption) SchemaOption {
|
|
return schemaSetter(func(schema *jsonschema.Schema) {
|
|
propSchema, ok := schema.Properties[name]
|
|
if !ok {
|
|
propSchema = new(jsonschema.Schema)
|
|
schema.Properties[name] = propSchema
|
|
}
|
|
// Apply the options, with special handling for Required, as it needs to be
|
|
// set on the parent schema.
|
|
for _, opt := range opts {
|
|
if req, ok := opt.(required); ok {
|
|
if req {
|
|
if !slices.Contains(schema.Required, name) {
|
|
schema.Required = append(schema.Required, name)
|
|
}
|
|
} else {
|
|
schema.Required = slices.DeleteFunc(schema.Required, func(s string) bool {
|
|
return s == name
|
|
})
|
|
}
|
|
} else {
|
|
opt.set(propSchema)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Required sets whether the associated property is required. It is only valid
|
|
// when used in a [Property] option: using Required outside of Property panics.
|
|
func Required(v bool) SchemaOption {
|
|
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
|
|
|
|
func (required) set(s *jsonschema.Schema) {
|
|
panic("use of required outside of Property")
|
|
}
|
|
|
|
// Enum sets the provided values as the "enum" value of the schema.
|
|
func Enum(values ...any) SchemaOption {
|
|
return schemaSetter(func(s *jsonschema.Schema) {
|
|
s.Enum = values
|
|
})
|
|
}
|
|
|
|
// Description sets the provided schema description.
|
|
func Description(desc string) SchemaOption {
|
|
return description(desc)
|
|
}
|
|
|
|
// 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.
|
|
func Schema(schema *jsonschema.Schema) SchemaOption {
|
|
return schemaSetter(func(s *jsonschema.Schema) {
|
|
*s = *schema
|
|
})
|
|
}
|