Add support for all content types, and formalize the conversion of content to and from the wire format (as a discriminated union). Change-Id: I93678ce98c02176a524d2c82425fb1267ad68bf0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/669135 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com> Auto-Submit: Robert Findley <rfindley@google.com>
395 lines
11 KiB
Go
395 lines
11 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.
|
|
|
|
//go:build ignore
|
|
|
|
// This script generates protocol definitions in protocol.go from the MCP spec.
|
|
//
|
|
// Only the set of declarations configured by the [declarations] value are
|
|
// generated.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"go/format"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
|
|
"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")
|
|
|
|
// A typeConfig defines a rewrite to perform to a (possibly nested) struct
|
|
// field. In some cases, we may want to use an external type for the nested
|
|
// struct field. In others, we may want to extract the type definition to a
|
|
// name.
|
|
type typeConfig struct {
|
|
Name string // declaration name for the type
|
|
Substitute string // type definition to substitute
|
|
Fields config // individual field configuration, or nil
|
|
}
|
|
|
|
type config map[string]*typeConfig
|
|
|
|
// declarations configures the set of declarations to write.
|
|
//
|
|
// Top level declarations are only created if they are configured with a
|
|
// non-empty Name. Otherwise, they are discarded, though their fields may be
|
|
// extracted to types if they have a nested field configuration.
|
|
var declarations = config{
|
|
"Annotations": {Name: "Annotations"},
|
|
"CallToolRequest": {
|
|
Fields: config{"Params": {Name: "CallToolParams"}},
|
|
},
|
|
"CallToolResult": {Name: "CallToolResult"},
|
|
"CancelledNotification": {
|
|
Fields: config{"Params": {Name: "CancelledParams"}},
|
|
},
|
|
"ClientCapabilities": {Name: "ClientCapabilities"},
|
|
"GetPromptRequest": {
|
|
Fields: config{"Params": {Name: "GetPromptParams"}},
|
|
},
|
|
"GetPromptResult": {Name: "GetPromptResult"},
|
|
"Implementation": {Name: "Implementation"},
|
|
"InitializeRequest": {
|
|
Fields: config{"Params": {Name: "InitializeParams"}},
|
|
},
|
|
"InitializeResult": {Name: "InitializeResult"},
|
|
"InitializedNotification": {
|
|
Fields: config{"Params": {Name: "InitializedParams"}},
|
|
},
|
|
"ListPromptsRequest": {
|
|
Fields: config{"Params": {Name: "ListPromptsParams"}},
|
|
},
|
|
"ListPromptsResult": {Name: "ListPromptsResult"},
|
|
"ListToolsRequest": {
|
|
Fields: config{"Params": {Name: "ListToolsParams"}},
|
|
},
|
|
"ListToolsResult": {Name: "ListToolsResult"},
|
|
"Prompt": {Name: "Prompt"},
|
|
"PromptMessage": {Name: "PromptMessage"},
|
|
"PromptArgument": {Name: "PromptArgument"},
|
|
"RequestId": {Substitute: "any"}, // null|number|string
|
|
"Role": {Name: "Role"},
|
|
"ServerCapabilities": {
|
|
Name: "ServerCapabilities",
|
|
Fields: config{
|
|
"Prompts": {Name: "PromptCapabilities"},
|
|
"Resources": {Name: "ResourceCapabilities"},
|
|
"Tools": {Name: "ToolCapabilities"},
|
|
},
|
|
},
|
|
"Tool": {
|
|
Name: "Tool",
|
|
Fields: config{"InputSchema": {Substitute: "*jsonschema.Schema"}},
|
|
},
|
|
"ToolAnnotations": {Name: "ToolAnnotations"},
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
// Load and unmarshal the schema.
|
|
data, err := loadSchema(*schemaFile)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
schema := new(jsonschema.Schema)
|
|
if err := json.Unmarshal(data, &schema); 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
|
|
// also allows us to sort.
|
|
named := make(map[string]*bytes.Buffer)
|
|
for name, def := range util.Sorted(schema.Definitions) {
|
|
config := declarations[name]
|
|
if config == nil {
|
|
continue
|
|
}
|
|
if err := writeDecl(*config, def, named); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
fmt.Fprintf(buf, `
|
|
// 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.
|
|
|
|
// Code generated by generate.go. DO NOT EDIT.
|
|
|
|
package protocol
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/internal/jsonschema"
|
|
)
|
|
`)
|
|
|
|
// Write out types.
|
|
for _, b := range util.Sorted(named) {
|
|
fmt.Fprintln(buf)
|
|
fmt.Fprint(buf, b.String())
|
|
}
|
|
|
|
formatted, err := format.Source(buf.Bytes())
|
|
if err != nil {
|
|
log.Println(buf.String())
|
|
log.Fatalf("failed to format: %v", err)
|
|
}
|
|
if err := os.WriteFile("protocol.go", formatted, 0666); err != nil {
|
|
log.Fatalf("failed to write protocol.go: %v", err)
|
|
}
|
|
}
|
|
|
|
func loadSchema(schemaFile string) (data []byte, err error) {
|
|
const schemaURL = "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/2025-03-26/schema.json"
|
|
|
|
if schemaFile != "" {
|
|
data, err = os.ReadFile(schemaFile)
|
|
if os.IsNotExist(err) {
|
|
data = nil
|
|
} else if err != nil {
|
|
return nil, fmt.Errorf("reading schema file %q: %v", schemaFile, err)
|
|
}
|
|
}
|
|
if data == nil {
|
|
resp, err := http.Get(schemaURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("downloading schema: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("downloading schema: %v", resp.Status)
|
|
}
|
|
data, err = io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading schema body: %v", err)
|
|
}
|
|
if schemaFile != "" {
|
|
if err := os.WriteFile(schemaFile, data, 0666); err != nil {
|
|
return nil, fmt.Errorf("persisting schema: %v", err)
|
|
}
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func writeDecl(config typeConfig, def *jsonschema.Schema, named map[string]*bytes.Buffer) error {
|
|
var w io.Writer = io.Discard
|
|
if name := config.Name; name != "" {
|
|
if _, ok := named[name]; ok {
|
|
return nil
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
w = buf
|
|
named[name] = buf
|
|
if def.Description != "" {
|
|
fmt.Fprintf(buf, "%s\n", toComment(def.Description))
|
|
}
|
|
fmt.Fprintf(buf, "type %s ", name)
|
|
}
|
|
if err := writeType(w, &config, def, named); err != nil {
|
|
return err // Better error here?
|
|
}
|
|
fmt.Fprintf(w, "\n")
|
|
return nil
|
|
}
|
|
|
|
// writeType writes the type definition to the given writer.
|
|
//
|
|
// If path is non-empty, it is the path to the field using this type, for the
|
|
// purpose of detecting field rewrites (see [fieldRewrite]).
|
|
//
|
|
// named is the in-progress collection of type definitions. New named types may
|
|
// 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 != "" {
|
|
// 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.
|
|
if cfg, ok := declarations[name]; ok {
|
|
if cfg.Name == "" && cfg.Substitute == "" {
|
|
panic(fmt.Sprintf("referenced type %q cannot be referred to (no name or substitution)", name))
|
|
}
|
|
if cfg.Substitute != "" {
|
|
name = cfg.Substitute
|
|
} else {
|
|
name = cfg.Name
|
|
}
|
|
}
|
|
w.Write([]byte(name))
|
|
return nil
|
|
}
|
|
|
|
// For types that explicitly allow additional properties, we can either
|
|
// unmarshal them into a map[string]any, or delay unmarshalling with
|
|
// json.RawMessage. For now, use json.RawMessage as it defers the choice.
|
|
if def.Type == "object" && canHaveAdditionalProperties(def) {
|
|
w.Write([]byte("map[string]"))
|
|
return writeType(w, nil, def.AdditionalProperties, named)
|
|
}
|
|
|
|
if def.Type == "" {
|
|
// special case: recognize Content
|
|
if slices.ContainsFunc(def.AnyOf, func(s *jsonschema.Schema) bool {
|
|
return s.Ref == "#/definitions/TextContent"
|
|
}) {
|
|
fmt.Fprintf(w, "Content")
|
|
} else {
|
|
// E.g. union types.
|
|
fmt.Fprintf(w, "json.RawMessage")
|
|
}
|
|
} else {
|
|
switch def.Type {
|
|
case "array":
|
|
fmt.Fprintf(w, "[]")
|
|
return writeType(w, nil, def.Items, named)
|
|
|
|
case "boolean":
|
|
fmt.Fprintf(w, "bool")
|
|
|
|
case "integer":
|
|
fmt.Fprintf(w, "int64")
|
|
|
|
// not handled: "null"
|
|
|
|
case "number":
|
|
// We could use json.Number here; use float64 for simplicity.
|
|
fmt.Fprintf(w, "float64")
|
|
|
|
case "object":
|
|
fmt.Fprintf(w, "struct {\n")
|
|
for name, fieldDef := range util.Sorted(def.Properties) {
|
|
if fieldDef.Description != "" {
|
|
fmt.Fprintf(w, "%s\n", toComment(fieldDef.Description))
|
|
}
|
|
export := exportName(name)
|
|
fmt.Fprintf(w, "\t%s ", export)
|
|
|
|
required := slices.Contains(def.Required, name)
|
|
|
|
// If the field is not required, and 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))
|
|
|
|
if config != nil && config.Fields[export] != nil {
|
|
r := config.Fields[export]
|
|
if r.Substitute != "" {
|
|
fmt.Fprintf(w, r.Substitute)
|
|
} else {
|
|
assert(r.Name != "", "missing ExtractTo")
|
|
if err := writeDecl(*r, fieldDef, named); err != nil {
|
|
return err
|
|
}
|
|
if needPointer {
|
|
fmt.Fprintf(w, "*")
|
|
}
|
|
fmt.Fprintf(w, r.Name)
|
|
}
|
|
} 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)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, " `json:\"%s", name)
|
|
if !required {
|
|
fmt.Fprint(w, ",omitempty")
|
|
}
|
|
fmt.Fprint(w, "\"`\n")
|
|
}
|
|
fmt.Fprintf(w, "}")
|
|
|
|
case "string":
|
|
fmt.Fprintf(w, "string")
|
|
|
|
default:
|
|
fmt.Fprintf(w, "any")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// toComment converts a JSON schema description to a Go comment.
|
|
func toComment(description string) string {
|
|
var (
|
|
buf strings.Builder
|
|
lineBuf strings.Builder
|
|
)
|
|
const wrapAt = 80
|
|
for line := range strings.SplitSeq(description, "\n") {
|
|
// Start a new paragraph, if the current is nonempty.
|
|
if len(line) == 0 && lineBuf.Len() > 0 {
|
|
buf.WriteString(lineBuf.String())
|
|
lineBuf.Reset()
|
|
buf.WriteString("\n//\n")
|
|
continue
|
|
}
|
|
// Otherwise, fill in the current paragraph.
|
|
for field := range strings.FieldsSeq(line) {
|
|
if lineBuf.Len() > 0 && lineBuf.Len()+len(" ")+len(field) > wrapAt {
|
|
buf.WriteString(lineBuf.String())
|
|
buf.WriteRune('\n')
|
|
lineBuf.Reset()
|
|
}
|
|
if lineBuf.Len() == 0 {
|
|
lineBuf.WriteString("//")
|
|
}
|
|
lineBuf.WriteString(" ")
|
|
lineBuf.WriteString(field)
|
|
}
|
|
}
|
|
if lineBuf.Len() > 0 {
|
|
buf.WriteString(lineBuf.String())
|
|
}
|
|
return strings.TrimRight(buf.String(), "\n")
|
|
}
|
|
|
|
// The MCP spec improperly uses the absence of the additionalProperties keyword to
|
|
// mean that additional properties are not allowed. In fact, it means just the opposite
|
|
// (https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.6.5.6).
|
|
// If the MCP spec wants to allow additional properties, it will write "true" or
|
|
// an object explicitly.
|
|
func canHaveAdditionalProperties(s *jsonschema.Schema) bool {
|
|
ap := s.AdditionalProperties
|
|
return ap != nil && !reflect.DeepEqual(ap, &jsonschema.Schema{Not: &jsonschema.Schema{}})
|
|
}
|
|
|
|
// exportName returns an exported name for a Go symbol, based on the given name
|
|
// in the JSON schema, removing leading underscores and capitalizing.
|
|
func exportName(s string) string {
|
|
if strings.HasPrefix(s, "_") {
|
|
s = s[1:]
|
|
}
|
|
return strings.ToUpper(s[:1]) + s[1:]
|
|
}
|
|
|
|
func assert(cond bool, msg string) {
|
|
if !cond {
|
|
panic(msg)
|
|
}
|
|
}
|