Add validation tools for server.json schemas and examples (#176)

This is a first PR for #159 - adding some validation scripts. I want to
get to a place where we are confident that making schema changes is in
sync with the rest of our code. Step one is making sure the schemas are
in sync with our examples. Step two will be making sure the schemas are
in sync with our actual Registry implementation code.

This is just for the JSON right now; if we're feeling good about this
approach I'll do an analogous change for the OpenAPI schemas as well.

I'll also follow with adding usage of these scripts to our CI pipeline
so we have automated checks on them with every change.

## Summary
- Added CLI validation tools for JSON schemas and examples in
`docs/server-json/`
- Fixed some minor validation errors found in `examples.md`
- Ensures schema consistency and example validity in CI/CD pipelines

## Changes

### New Validation Tools
1. **`tools/validate-schemas`** - Validates that `schema.json` and
`registry-schema.json` are valid JSON Schema documents
2. **`tools/validate-examples`** - Validates all JSON examples in
`examples.md` against both schemas

### Key Features
- ✅ Validates JSON schema syntax and structure
- ✅ Validates all examples against both base and registry schemas
- ✅ Tracks validation counts to prevent silent failures
- ✅ Proper exit codes for CI integration
- ✅ Clear error messages for debugging

### Fixes to examples.md
- Added missing `repository.id` field in NuGet example
- Changed `repository.source` from "gitlab" to "github" in Docker
example

### Usage
```bash
# From repository root
./tools/validate-schemas.sh
./tools/validate-examples.sh
```

## Testing

I ran these scripts locally with minor (breaking) changes, which they
each correctly reported. As submitted, they are both passing.

🤖 Co-authored with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Charles Lowell <10964656+chlowell@users.noreply.github.com>
This commit is contained in:
Tadas Antanavicius 2025-07-15 11:32:54 -07:00 • committed by GitHub
commit 774a0df592
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 321 additions and 11 deletions

View file

@ -64,7 +64,7 @@ func StartAuthHandler(authService auth.Service) http.HandlerFunc {
// Return successful response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(map[string]interface{}{
if err := json.NewEncoder(w).Encode(map[string]any{
"flow_info": flowInfo,
"status_token": statusToken,
"expires_in": 300, // 5 minutes
@ -98,7 +98,7 @@ func CheckAuthStatusHandler(authService auth.Service) http.HandlerFunc {
// Auth is still pending
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(map[string]interface{}{
if err := json.NewEncoder(w).Encode(map[string]any{
"status": "pending",
}); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
@ -115,7 +115,7 @@ func CheckAuthStatusHandler(authService auth.Service) http.HandlerFunc {
// Authentication completed successfully
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(map[string]interface{}{
if err := json.NewEncoder(w).Encode(map[string]any{
"status": "complete",
"token": token,
}); err != nil {

View file

@ -61,7 +61,7 @@ func TestPublishHandler(t *testing.T) {
testCases := []struct {
name string
method string
requestBody interface{}
requestBody any
authHeader string
setupMocks func(*MockRegistryService, *MockAuthService)
expectedStatus int

View file

@ -19,7 +19,7 @@ var (
// Database defines the interface for database operations on MCPRegistry entries
type Database interface {
// List retrieves all MCPRegistry entries with optional filtering
List(ctx context.Context, filter map[string]interface{}, cursor string, limit int) ([]*model.Server, string, error)
List(ctx context.Context, filter map[string]any, cursor string, limit int) ([]*model.Server, string, error)
// GetByID retrieves a single ServerDetail by it's ID
GetByID(ctx context.Context, id string) (*model.ServerDetail, error)
// Publish adds a new ServerDetail to the database

View file

@ -33,7 +33,7 @@ func ReadSeedFile(path string) ([]model.ServerDetail, error) {
var servers []model.ServerDetail
if err := json.Unmarshal(fileContent, &servers); err != nil {
// Try parsing as a raw JSON array and then convert to our model
var rawData []map[string]interface{}
var rawData []map[string]any
if jsonErr := json.Unmarshal(fileContent, &rawData); jsonErr != nil {
return nil, fmt.Errorf("failed to parse JSON: %w (original error: %w)", jsonErr, err)
}

View file

@ -90,7 +90,7 @@ func compareSemanticVersions(version1, version2 string) int {
//gocognit:ignore
func (db *MemoryDB) List(
ctx context.Context,
filter map[string]interface{},
filter map[string]any,
cursor string,
limit int,
) ([]*model.Server, string, error) {

View file

@ -75,7 +75,7 @@ func NewMongoDB(ctx context.Context, connectionURI, databaseName, collectionName
// List retrieves MCPRegistry entries with optional filtering and pagination
func (db *MongoDB) List(
ctx context.Context,
filter map[string]interface{},
filter map[string]any,
cursor string,
limit int,
) ([]*model.Server, string, error) {