internal/mcp: reorganize the tool API, keeping generics

CallToolParams.Arguments is any.

Clients can send whatever they want.

Tool authors who create their own ToolHandler (bypassing NewTool)
will receive the arguments as a map[string]any which has been
validated against the input schema.

Tool authors who call NewTool can choose the type that the arguments
unmarshal into.

There is no double-unmarshaling and no downcasting.

There is no way for tool authors to avoid the unmarshal, and no way to
avoid the validation unless the omit an input schema, which is probably
bad for LLMs. If tool authors want this optimization, we can provide
it later.

Change-Id: Ieb201e56c7fb8a23bb1a93b27e946630ec9e79ad
Reviewed-on: https://go-review.googlesource.com/c/tools/+/679595
TryBot-Bypass: Jonathan Amsterdam <jba@google.com>
Auto-Submit: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Jonathan Amsterdam 2025-06-05 16:17:03 -04:00
commit f211cc8e4a

View file

@ -58,10 +58,9 @@ var declarations = config{
Name: "-",
Fields: config{
"Params": {
Name: "CallToolParams",
TypeParams: [][2]string{{"TArgs", "any"}},
Name: "CallToolParams",
Fields: config{
"Arguments": {Substitute: "TArgs"},
"Arguments": {Substitute: "any"},
},
},
},
@ -307,6 +306,9 @@ func loadSchema(schemaFile string) (data []byte, err error) {
return data, nil
}
// Suffixes of type names with a meta field.
var metaSuffixes = []string{"Params", "ParamsFor", "Result", "ResultFor"}
func writeDecl(configName string, config typeConfig, def *jsonschema.Schema, named map[string]*bytes.Buffer) error {
var w io.Writer = io.Discard
var typeName string
@ -321,8 +323,11 @@ func writeDecl(configName string, config typeConfig, def *jsonschema.Schema, nam
// Every Params and Result type should have a _meta property.
// Also, those with a progress token will turn into a struct; we want the progress token to
// be a map item. So replace all metas.
if strings.HasSuffix(typeName, "Params") || strings.HasSuffix(typeName, "Result") {
def.Properties["_meta"] = metaSchema
for _, s := range metaSuffixes {
if strings.HasSuffix(typeName, s) {
def.Properties["_meta"] = metaSchema
break
}
}
buf := new(bytes.Buffer)
w = buf
@ -371,6 +376,28 @@ func writeDecl(configName string, config typeConfig, def *jsonschema.Schema, nam
fmt.Fprintf(w, "\nfunc (x *%s) nextCursorPtr() *string { return &x.NextCursor }", typeName)
}
// Some types have generic counterparts.
var typeArg, substField string
switch typeName {
case "CallToolParams":
typeName = "CallToolParamsFor"
typeArg = "In"
substField = "Arguments"
case "CallToolResult":
typeName = "CallToolResultFor"
typeArg = "Out"
substField = "StructuredContent"
}
if typeArg != "" {
fmt.Fprintf(w, "\n\ntype %s[%s any]", typeName, typeArg)
if config.Fields != nil {
config.Fields[substField].Substitute = typeArg
}
if err := writeType(w, &config, def, named); err != nil {
return err
}
fmt.Fprintf(w, "\nfunc (x *%s[%s]) GetMeta() *Meta {return &x.Meta}", typeName, typeArg)
}
return nil
}