//go:build ignore // check-endpoint-drift compares the current OpenAPI spec against the endpoint manifest // and reports any added or removed endpoints. Exit 0 if no drift, exit 1 if drift detected. package main import ( "encoding/json" "fmt" "os" "path/filepath" "runtime" "sort" "strings" "gopkg.in/yaml.v3" ) type Endpoint struct { Method string `json:"method" yaml:"-"` Path string `json:"path" yaml:"-"` Summary string `json:"summary" yaml:"-"` Tag string `json:"tag" yaml:"-"` } func endpointKey(e Endpoint) string { return e.Method + " " + e.Path } type OpenAPISpec struct { Paths map[string]map[string]struct { Summary string `yaml:"summary"` Tags []string `yaml:"tags"` } `yaml:"paths"` } func extractEndpoints(specPath string) ([]Endpoint, error) { data, err := os.ReadFile(specPath) if err != nil { return nil, fmt.Errorf("reading spec: %w", err) } var spec OpenAPISpec if err := yaml.Unmarshal(data, &spec); err != nil { return nil, fmt.Errorf("parsing spec: %w", err) } validMethods := map[string]bool{ "get": true, "post": true, "put": true, "delete": true, "patch": true, } var endpoints []Endpoint for path, methods := range spec.Paths { for method, details := range methods { if !validMethods[strings.ToLower(method)] { continue } tag := "Untagged" if len(details.Tags) > 0 { tag = details.Tags[0] } endpoints = append(endpoints, Endpoint{ Method: strings.ToUpper(method), Path: path, Summary: details.Summary, Tag: tag, }) } } sort.Slice(endpoints, func(i, j int) bool { if endpoints[i].Path != endpoints[j].Path { return endpoints[i].Path < endpoints[j].Path } return endpoints[i].Method < endpoints[j].Method }) return endpoints, nil } func main() { // Resolve paths relative to this script's location _, filename, _, _ := runtime.Caller(0) rootDir := filepath.Dir(filepath.Dir(filename)) specPath := filepath.Join(rootDir, "openapi.yaml") manifestPath := filepath.Join(rootDir, "endpoint-manifest.json") current, err := extractEndpoints(specPath) if err != nil { fmt.Fprintf(os.Stderr, "Error extracting endpoints: %v\n", err) os.Exit(1) } manifestData, err := os.ReadFile(manifestPath) if err != nil { fmt.Fprintf(os.Stderr, "Error: endpoint-manifest.json not found. Copy it from the MCP repo first.\n") os.Exit(1) } var manifest []Endpoint if err := json.Unmarshal(manifestData, &manifest); err != nil { fmt.Fprintf(os.Stderr, "Error parsing manifest: %v\n", err) os.Exit(1) } manifestKeys := make(map[string]bool) for _, e := range manifest { manifestKeys[endpointKey(e)] = true } currentKeys := make(map[string]bool) for _, e := range current { currentKeys[endpointKey(e)] = true } var added, removed []Endpoint for _, e := range current { if !manifestKeys[endpointKey(e)] { added = append(added, e) } } for _, e := range manifest { if !currentKeys[endpointKey(e)] { removed = append(removed, e) } } if len(added) == 0 && len(removed) == 0 { fmt.Printf("No endpoint drift detected. %d endpoints match the manifest.\n", len(current)) os.Exit(0) } fmt.Println("Endpoint drift detected!") fmt.Println() if len(added) > 0 { fmt.Printf("New endpoints (%d):\n", len(added)) for _, e := range added { fmt.Printf(" + %s %s — %s [%s]\n", e.Method, e.Path, e.Summary, e.Tag) } fmt.Println() } if len(removed) > 0 { fmt.Printf("Removed endpoints (%d):\n", len(removed)) for _, e := range removed { fmt.Printf(" - %s %s — %s [%s]\n", e.Method, e.Path, e.Summary, e.Tag) } fmt.Println() } fmt.Println("Update endpoint-manifest.json from the MCP repo to resolve drift.") os.Exit(1) }