registry/scripts/mirror_data/fetch_production_data.go
Tadas Antanavicius 2d49e5b68c
fix: prevent NULL status values during migration 008 (#587)
## Summary
- Fixes migration 008 failing with "column contains null values" error
- Adds COALESCE protection to handle 126 servers without status fields
- Includes test utilities to reproduce and verify fixes with production
data

## Problem

Migration 008 was failing in production because:
- 126 out of 781 servers have no `status` field in their JSON
- When extracting `rec.value->>'status'`, these became NULL values
- The `ALTER TABLE servers ALTER COLUMN status SET NOT NULL` constraint
then failed

## Solution

1. **Prevent NULLs during migration**: Use COALESCE to provide 'active'
default when status is NULL/empty
2. **Enhanced debugging**: Added RAISE NOTICE statements to log
problematic values
3. **Safety check**: Double-check that throws exception if any NULLs
remain
4. **Test utilities**: Created mirror_data tools to test migrations with
real production data

## Testing

Created and used `scripts/mirror_data/` tools to:
- Fetch all 781 servers from production API
- Load them into a test database
- Verify the migration fix works correctly

### Results:
- **Before fix**: 126 NULL status values created during migration
- **After fix**: 0 NULL status values, all servers have valid status
- Migration completes successfully with NOT NULL constraint

## Test plan
- [x] Tested locally with production data using mirror_data tools
- [x] Verified 0 NULL values after migration (vs 126 before)
- [x] Confirmed NOT NULL constraint adds successfully
- [ ] Deploy to staging and verify migration succeeds
- [ ] Monitor production deployment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-30 06:43:26 -07:00

87 lines
2 KiB
Go

// This tool was created by Claude Code as a simple way to kick the tires on data migrations
// by fetching production data from the public registry API.
// It is not intended for production use.
//
//go:build ignore
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
type ServerResponse struct {
Servers []json.RawMessage `json:"servers"`
Metadata struct {
NextCursor string `json:"next_cursor,omitempty"`
Count int `json:"count"`
} `json:"metadata"`
}
func main() {
baseURL := "https://registry.modelcontextprotocol.io/v0/servers"
var allServers []json.RawMessage
cursor := ""
pageCount := 0
for {
pageCount++
url := baseURL
if cursor != "" {
url = fmt.Sprintf("%s?cursor=%s", baseURL, cursor)
}
fmt.Printf("Fetching page %d: %s\n", pageCount, url)
resp, err := http.Get(url)
if err != nil {
panic(fmt.Sprintf("Failed to fetch: %v", err))
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(fmt.Sprintf("Failed to read body: %v", err))
}
var serverResp ServerResponse
if err := json.Unmarshal(body, &serverResp); err != nil {
panic(fmt.Sprintf("Failed to parse JSON: %v", err))
}
allServers = append(allServers, serverResp.Servers...)
fmt.Printf(" Got %d servers (total: %d)\n", len(serverResp.Servers), len(allServers))
if serverResp.Metadata.NextCursor == "" {
break
}
cursor = serverResp.Metadata.NextCursor
// Be nice to the API
time.Sleep(100 * time.Millisecond)
}
// Save all servers to a file
output := map[string]interface{}{
"servers": allServers,
"count": len(allServers),
"fetched": time.Now().Format(time.RFC3339),
}
data, err := json.MarshalIndent(output, "", " ")
if err != nil {
panic(fmt.Sprintf("Failed to marshal output: %v", err))
}
outputFile := "production_servers.json"
if err := os.WriteFile(outputFile, data, 0644); err != nil {
panic(fmt.Sprintf("Failed to write file: %v", err))
}
fmt.Printf("\nDone! Saved %d servers to %s\n", len(allServers), outputFile)
}