// 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) } }