// 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" "cmp" "encoding/json" "flag" "fmt" "go/format" "io" "iter" "log" "net/http" "os" "slices" "strings" "github.com/modelcontextprotocol/go-sdk/internal/jsonschema" ) 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", }, "ClientCapabilities": {Name: "ClientCapabilities"}, "Implementation": {Name: "Implementation"}, "InitializeRequest": { Fields: config{"Params": {Name: "InitializeParams"}}, }, "InitializeResult": { Name: "InitializeResult", }, "InitializedNotification": { Fields: config{"Params": {Name: "InitializedParams"}}, }, "ListToolsRequest": { Fields: config{"Params": {Name: "ListToolsParams"}}, }, "ListToolsResult": { Name: "ListToolsResult", }, "Role": {Name: "Role"}, "ServerCapabilities": { Name: "ServerCapabilities", Fields: config{ "Prompts": {Name: "PromptCapabilities"}, "Resources": {Name: "ResourceCapabilities"}, "Tools": {Name: "ToolCapabilities"}, }, }, "TextContent": {Name: "TextContent"}, "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 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 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" && def.AdditionalProperties != nil && def.AdditionalProperties != false { w.Write([]byte("json.RawMessage")) return nil } switch typ := def.Type.(type) { case string: switch typ { 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 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" && (fieldDef.AdditionalProperties == nil || fieldDef.AdditionalProperties == false)) 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") } default: // E.g. union types. fmt.Fprintf(w, "json.RawMessage") } 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") } // 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) } } // 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 }