| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-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" |
| "maps" |
| "net/http" |
| "os" |
| "reflect" |
| "regexp" |
| "slices" |
| "strings" |
| |
| "golang.org/x/tools/internal/mcp/internal/util" |
| "golang.org/x/tools/internal/mcp/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 |
| TypeParams [][2]string // formatted type parameter list ({name, constraint}), if any |
| 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 created unless configured with Name=="-", |
| // in which case they are discarded, though their fields may be |
| // extracted to types if they have a nested field configuration. |
| // If Name == "", the map key is used as the type name. |
| var declarations = config{ |
| "Annotations": {}, |
| "CallToolRequest": { |
| Name: "-", |
| Fields: config{ |
| "Params": { |
| Name: "CallToolParams", |
| Fields: config{ |
| "Arguments": {Substitute: "any"}, |
| }, |
| }, |
| }, |
| }, |
| "CallToolResult": {}, |
| "CancelledNotification": { |
| Name: "-", |
| Fields: config{"Params": {Name: "CancelledParams"}}, |
| }, |
| "ClientCapabilities": { |
| Fields: config{"Sampling": {Name: "SamplingCapabilities"}}, |
| }, |
| "CreateMessageRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "CreateMessageParams"}}, |
| }, |
| "CreateMessageResult": {}, |
| "GetPromptRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "GetPromptParams"}}, |
| }, |
| "GetPromptResult": {}, |
| "Implementation": {Name: "implementation"}, |
| "InitializeRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "InitializeParams"}}, |
| }, |
| "InitializeResult": {Name: "InitializeResult"}, |
| "InitializedNotification": { |
| Name: "-", |
| Fields: config{"Params": {Name: "InitializedParams"}}, |
| }, |
| "ListPromptsRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ListPromptsParams"}}, |
| }, |
| "ListPromptsResult": {}, |
| "ListResourcesRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ListResourcesParams"}}, |
| }, |
| "ListResourcesResult": {}, |
| "ListResourceTemplatesRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ListResourceTemplatesParams"}}, |
| }, |
| "ListResourceTemplatesResult": {}, |
| "ListRootsRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ListRootsParams"}}, |
| }, |
| "ListRootsResult": {}, |
| "ListToolsRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ListToolsParams"}}, |
| }, |
| "ListToolsResult": {}, |
| "loggingCapabilities": {Substitute: "struct{}"}, |
| "LoggingLevel": {}, |
| "LoggingMessageNotification": { |
| Name: "-", |
| Fields: config{ |
| "Params": { |
| Name: "LoggingMessageParams", |
| Fields: config{"Data": {Substitute: "any"}}, |
| }, |
| }, |
| }, |
| "ModelHint": {}, |
| "ModelPreferences": {}, |
| "PingRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "PingParams"}}, |
| }, |
| "Prompt": {}, |
| "PromptMessage": {}, |
| "PromptArgument": {}, |
| "PromptListChangedNotification": { |
| Name: "-", |
| Fields: config{"Params": {Name: "PromptListChangedParams"}}, |
| }, |
| "ProgressNotification": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ProgressNotificationParams"}}, |
| }, |
| "ProgressToken": {Name: "-", Substitute: "any"}, // null|number|string |
| "RequestId": {Name: "-", Substitute: "any"}, // null|number|string |
| "ReadResourceRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ReadResourceParams"}}, |
| }, |
| "ReadResourceResult": { |
| Fields: config{"Contents": {Substitute: "[]*ResourceContents"}}, |
| }, |
| "Resource": {}, |
| "ResourceListChangedNotification": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ResourceListChangedParams"}}, |
| }, |
| "ResourceTemplate": {}, |
| "ResourceTemplateListChangedNotification": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ResourceTemplateListChangedParams"}}, |
| }, |
| "Role": {}, |
| "Root": {}, |
| "RootsListChangedNotification": { |
| Name: "-", |
| Fields: config{"Params": {Name: "RootsListChangedParams"}}, |
| }, |
| |
| "SamplingCapabilities": {Substitute: "struct{}"}, |
| "SamplingMessage": {}, |
| "ServerCapabilities": { |
| Name: "serverCapabilities", |
| Fields: config{ |
| "Prompts": {Name: "promptCapabilities"}, |
| "Resources": {Name: "resourceCapabilities"}, |
| "Tools": {Name: "toolCapabilities"}, |
| "Logging": {Name: "loggingCapabilities"}, |
| }, |
| }, |
| "SetLevelRequest": { |
| Name: "-", |
| Fields: config{"Params": {Name: "SetLevelParams"}}, |
| }, |
| "Tool": { |
| Fields: config{"InputSchema": {Substitute: "*jsonschema.Schema"}}, |
| }, |
| "ToolAnnotations": {}, |
| "ToolListChangedNotification": { |
| Name: "-", |
| Fields: config{"Params": {Name: "ToolListChangedParams"}}, |
| }, |
| } |
| |
| 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) |
| } |
| // Resolve the schema so we have the referents of all the Refs. |
| if _, err := schema.Resolve(nil); 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(name, *config, def, named); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| buf := new(bytes.Buffer) |
| fmt.Fprintf(buf, ` |
| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Code generated by generate.go. DO NOT EDIT. |
| |
| package mcp |
| |
| import ( |
| "golang.org/x/tools/internal/mcp/jsonschema" |
| ) |
| `) |
| |
| // Write out types. |
| for _, b := range util.Sorted(named) { |
| fmt.Fprintln(buf) |
| fmt.Fprint(buf, b.String()) |
| } |
| // Write out method names. |
| fmt.Fprintln(buf, `const (`) |
| for _, name := range slices.Sorted(maps.Keys(schema.Definitions)) { |
| prefix := "method" |
| method, found := strings.CutSuffix(name, "Request") |
| if !found { |
| prefix = "notification" |
| method, found = strings.CutSuffix(name, "Notification") |
| } |
| if found { |
| if ms, ok := schema.Definitions[name].Properties["method"]; ok { |
| if c := ms.Const; c != nil { |
| fmt.Fprintf(buf, "%s%s = %q\n", prefix, method, *c) |
| } |
| } |
| } |
| } |
| fmt.Fprintln(buf, `)`) |
| |
| 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 |
| } |
| |
| // 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 |
| if typeName = config.Name; typeName != "-" { |
| if typeName == "" { |
| typeName = configName |
| } |
| if _, ok := named[typeName]; ok { |
| return nil |
| } |
| // The JSON schema does not accurately represent the source of truth, which is typescript. |
| // 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. |
| for _, s := range metaSuffixes { |
| if strings.HasSuffix(typeName, s) { |
| def.Properties["_meta"] = metaSchema |
| break |
| } |
| } |
| buf := new(bytes.Buffer) |
| w = buf |
| named[typeName] = buf |
| if def.Description != "" { |
| fmt.Fprintf(buf, "%s\n", toComment(def.Description)) |
| } |
| typeParams := new(strings.Builder) |
| if len(config.TypeParams) > 0 { |
| typeParams.WriteByte('[') |
| for i, p := range config.TypeParams { |
| if i > 0 { |
| typeParams.WriteString(", ") |
| } |
| fmt.Fprintf(typeParams, "%s %s", p[0], p[1]) |
| } |
| typeParams.WriteByte(']') |
| } |
| fmt.Fprintf(buf, "type %s%s ", typeName, typeParams) |
| } |
| if err := writeType(w, &config, def, named); err != nil { |
| return err // Better error here? |
| } |
| fmt.Fprintf(w, "\n") |
| |
| // Any decl with a _meta field gets a GetMeta method. |
| if _, ok := def.Properties["_meta"]; ok { |
| targs := new(strings.Builder) |
| if len(config.TypeParams) > 0 { |
| targs.WriteByte('[') |
| for i, p := range config.TypeParams { |
| if i > 0 { |
| targs.WriteString(", ") |
| } |
| fmt.Fprintf(targs, "%s", p[0]) |
| } |
| targs.WriteByte(']') |
| } |
| fmt.Fprintf(w, "\nfunc (x *%s%s) GetMeta() *Meta { return &x.Meta }", typeName, targs) |
| } |
| |
| if _, ok := def.Properties["cursor"]; ok { |
| fmt.Fprintf(w, "\nfunc (x *%s) cursorPtr() *string { return &x.Cursor }", typeName) |
| } |
| if _, ok := def.Properties["nextCursor"]; ok { |
| 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 |
| } |
| |
| // 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. |
| name, resolved := deref(def) |
| if 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 if cfg.Name != "" { |
| name = cfg.Name |
| } |
| if isStruct(resolved) { |
| w.Write([]byte{'*'}) |
| } |
| } |
| 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. We use any. |
| if def.Type == "object" && canHaveAdditionalProperties(def) && def.Properties == nil { |
| 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, "any") |
| } |
| } 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)) |
| } |
| if name == "_meta" { |
| fmt.Fprintln(w, "\tMeta Meta `json:\"_meta,omitempty\"`") |
| continue |
| } |
| |
| export := exportName(name) |
| fmt.Fprintf(w, "\t%s ", export) |
| |
| required := slices.Contains(def.Required, name) |
| |
| // If the field is a struct type, indirect with a |
| // pointer so that it can be empty as defined by encoding/json. |
| // This also future-proofs against the struct getting large. |
| fieldTypeSchema := fieldDef |
| // If the schema is a reference, dereference it. |
| if _, rs := deref(fieldDef); rs != nil { |
| fieldTypeSchema = rs |
| } |
| needPointer := isStruct(fieldTypeSchema) |
| // Special case: there are no sampling or logging capabilities defined, |
| // but we want them to be structs for future expansion. |
| if !needPointer && (name == "sampling" || name == "logging") { |
| needPointer = true |
| } |
| 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") |
| typename := export |
| if r.Name != "" { |
| typename = r.Name |
| } |
| if err := writeDecl(typename, *r, fieldDef, named); err != nil { |
| return err |
| } |
| if needPointer { |
| fmt.Fprintf(w, "*") |
| } |
| fmt.Fprintf(w, typename) |
| } |
| } else 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. |
| // It also rewrites initialisms. |
| func exportName(s string) string { |
| if strings.HasPrefix(s, "_") { |
| s = s[1:] |
| } |
| s = strings.ToUpper(s[:1]) + s[1:] |
| // Replace an initialism if it is its own "word": see the init function below for |
| // a definition. |
| // There is probably a clever way to write this whole thing with one regexp and |
| // a Replace method, but it would be quite obscure. |
| // This doesn't have to be fast, because the first match will rarely succeed. |
| for ism, re := range initialisms { |
| replacement := strings.ToUpper(ism) |
| // Find the index of one match at a time, and replace. (We can't find all |
| // at once, because the replacement will change the indices.) |
| for { |
| if loc := re.FindStringIndex(s); loc != nil { |
| // Don't replace the rune after the initialism, if any. |
| end := loc[1] |
| if end < len(s) { |
| end-- |
| } |
| s = s[:loc[0]] + replacement + s[end:] |
| } else { |
| break |
| } |
| } |
| } |
| return s |
| } |
| |
| // deref dereferences s.Ref. |
| // If s.Ref refers to a schema in the Definitions section, deref |
| // returns the definition name and the associated schema. |
| // Otherwise, deref returns "", nil. |
| func deref(s *jsonschema.Schema) (name string, _ *jsonschema.Schema) { |
| name, ok := strings.CutPrefix(s.Ref, "#/definitions/") |
| if !ok { |
| return "", nil |
| } |
| return name, s.ResolvedRef() |
| } |
| |
| // isStruct reports whether s should be translated to a struct. |
| func isStruct(s *jsonschema.Schema) bool { |
| return s.Type == "object" && s.Properties != nil && !canHaveAdditionalProperties(s) |
| } |
| |
| // The schema for "_meta". |
| // We only need the description: the rest is a special case. |
| var metaSchema = &jsonschema.Schema{ |
| Description: "This property is reserved by the protocol to allow clients and servers to attach additional metadata to their responses.", |
| } |
| |
| // schemaJSON returns the JSON for s. |
| // For debugging. |
| func schemaJSON(s *jsonschema.Schema) string { |
| data, err := json.Marshal(s) |
| if err != nil { |
| return fmt.Sprintf("<jsonschema.Schema:%v>", err) |
| } |
| return string(data) |
| } |
| |
| // Map from initialism to the regexp that matches it. |
| var initialisms = map[string]*regexp.Regexp{ |
| "Id": nil, |
| "Url": nil, |
| "Uri": nil, |
| "Mime": nil, |
| } |
| |
| func init() { |
| for ism := range initialisms { |
| // Match ism if it is at the end, or followed by an uppercase letter or a number. |
| initialisms[ism] = regexp.MustCompile(ism + `($|[A-Z0-9])`) |
| } |
| } |
| |
| func assert(cond bool, msg string) { |
| if !cond { |
| panic(msg) |
| } |
| } |