Skip to content

Commit f7d70a0

Browse files
committed
Move the schema update script to a unit test
Signed-off-by: Radoslav Dimitrov <[email protected]>
1 parent 43507f5 commit f7d70a0

File tree

6 files changed

+139
-258
lines changed

6 files changed

+139
-258
lines changed

β€ŽTaskfile.ymlβ€Ž

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,6 @@ tasks:
260260
cmds:
261261
- ./{{.BUILD_DIR}}/registry-builder version
262262

263-
sync-schema:
264-
desc: Sync schema reference with Go dependency version
265-
cmds:
266-
- echo "πŸ”„ Syncing schema version with Go dependency..."
267-
- ./scripts/sync-schema-version.sh
268-
- echo "βœ… Schema sync complete. Run 'task validate' to verify."
269-
270263
watch:
271264
desc: Watch for changes and rebuild (requires entr)
272265
cmds:

β€Žpkg/registry/official.goβ€Ž

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,53 @@ func (or *OfficialRegistry) ValidateAgainstSchema() error {
7171
}
7272

7373
// validateRegistry validates a registry object against the schema
74-
// This validates each server entry against the upstream MCP server schema,
75-
// ensuring compatibility with the official MCP registry format
76-
func (*OfficialRegistry) validateRegistry(registry *ToolHiveRegistryType) error {
74+
// This validates both the wrapper structure and each server entry
75+
func (or *OfficialRegistry) validateRegistry(registry *ToolHiveRegistryType) error {
76+
var allErrors []string
77+
78+
// Step 1: Validate the wrapper structure against the ToolHive registry schema
79+
registryJSON, err := json.Marshal(registry)
80+
if err != nil {
81+
return fmt.Errorf("failed to marshal registry: %w", err)
82+
}
83+
84+
// Load the wrapper schema from local file
85+
wrapperSchemaPath := "schemas/registry.schema.json"
86+
wrapperSchemaLoader := gojsonschema.NewReferenceLoader("file://" + wrapperSchemaPath)
87+
88+
wrapperLoader := gojsonschema.NewBytesLoader(registryJSON)
89+
wrapperResult, err := gojsonschema.Validate(wrapperSchemaLoader, wrapperLoader)
90+
if err != nil {
91+
return fmt.Errorf("wrapper schema validation failed: %w", err)
92+
}
93+
94+
if !wrapperResult.Valid() {
95+
for _, desc := range wrapperResult.Errors() {
96+
allErrors = append(allErrors, fmt.Sprintf("wrapper: %s", desc.String()))
97+
}
98+
}
99+
100+
// Step 2: Validate each server individually against the upstream MCP server schema
101+
if err := or.validateServers(registry.Data.Servers, &allErrors); err != nil {
102+
return err
103+
}
104+
105+
if len(allErrors) > 0 {
106+
return fmt.Errorf("validation errors: %v", allErrors)
107+
}
108+
109+
return nil
110+
}
111+
112+
// validateServers validates each server entry against the upstream MCP server schema
113+
// This function can be used standalone to validate individual servers
114+
func (*OfficialRegistry) validateServers(servers []upstream.ServerJSON, allErrors *[]string) error {
77115
// Use the upstream schema URL directly from the registry package
78116
// This ensures we're always validating against the same schema version
79117
// that the code is built with, eliminating the need for manual schema syncing
80-
schemaLoader := gojsonschema.NewReferenceLoader(model.CurrentSchemaURL)
118+
serverSchemaLoader := gojsonschema.NewReferenceLoader(model.CurrentSchemaURL)
81119

82-
// Validate each server individually against the upstream schema
83-
var allErrors []string
84-
for i, server := range registry.Data.Servers {
120+
for i, server := range servers {
85121
// Marshal server to JSON
86122
serverJSON, err := json.Marshal(server)
87123
if err != nil {
@@ -92,22 +128,18 @@ func (*OfficialRegistry) validateRegistry(registry *ToolHiveRegistryType) error
92128
documentLoader := gojsonschema.NewBytesLoader(serverJSON)
93129

94130
// Perform validation
95-
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
131+
result, err := gojsonschema.Validate(serverSchemaLoader, documentLoader)
96132
if err != nil {
97133
return fmt.Errorf("schema validation failed for server %d (%s): %w", i, server.Name, err)
98134
}
99135

100136
if !result.Valid() {
101137
for _, desc := range result.Errors() {
102-
allErrors = append(allErrors, fmt.Sprintf("data.servers.%d: %s", i, desc.String()))
138+
*allErrors = append(*allErrors, fmt.Sprintf("data.servers.%d: %s", i, desc.String()))
103139
}
104140
}
105141
}
106142

107-
if len(allErrors) > 0 {
108-
return fmt.Errorf("validation errors: %v", allErrors)
109-
}
110-
111143
return nil
112144
}
113145

@@ -304,6 +336,7 @@ func (*OfficialRegistry) createRemotes(entry *types.RegistryEntry) []model.Trans
304336

305337

306338
// createXPublisherExtensions creates x-publisher extensions with ToolHive-specific data
339+
// Following the reverse DNS naming convention: io.github.stacklok
307340
func (or *OfficialRegistry) createXPublisherExtensions(entry *types.RegistryEntry) map[string]interface{} {
308341
// Get the key for the ToolHive extensions (image or URL)
309342
var key string
@@ -318,8 +351,9 @@ func (or *OfficialRegistry) createXPublisherExtensions(entry *types.RegistryEntr
318351
// Create ToolHive-specific extensions
319352
toolhiveExtensions := or.createToolHiveExtensions(entry)
320353

354+
// Use reverse DNS naming convention for vendor-specific data
321355
return map[string]interface{}{
322-
"toolhive": map[string]interface{}{
356+
"io.github.stacklok": map[string]interface{}{
323357
key: toolhiveExtensions,
324358
},
325359
}
@@ -332,6 +366,9 @@ func (or *OfficialRegistry) createToolHiveExtensions(entry *types.RegistryEntry)
332366
// Always include transport type
333367
extensions["transport"] = entry.GetTransport()
334368

369+
// Add status (active/deprecated)
370+
extensions["status"] = string(or.convertStatus(entry.GetStatus()))
371+
335372
// Add tools list
336373
if tools := entry.GetTools(); len(tools) > 0 {
337374
extensions["tools"] = tools
@@ -499,6 +536,6 @@ func (*OfficialRegistry) convertNameToReverseDNS(name string) string {
499536
return name
500537
}
501538

502-
// Convert simple names to toolhive namespace format
503-
return "io.stacklok.toolhive/" + name
539+
// Convert simple names to GitHub-based namespace format
540+
return "io.github.stacklok/" + name
504541
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package registry
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"regexp"
7+
"testing"
8+
9+
"github.com/modelcontextprotocol/registry/pkg/model"
10+
)
11+
12+
// TestSchemaVersionSync ensures that the schema reference in registry.schema.json
13+
// matches the schema version from the Go package (model.CurrentSchemaVersion).
14+
// This prevents schema drift when upgrading the registry package.
15+
func TestSchemaVersionSync(t *testing.T) {
16+
// Read the schema file
17+
schemaPath := "../../schemas/registry.schema.json"
18+
schemaData, err := os.ReadFile(schemaPath)
19+
if err != nil {
20+
t.Fatalf("Failed to read schema file: %v", err)
21+
}
22+
23+
// Parse the schema JSON
24+
var schema map[string]interface{}
25+
if err := json.Unmarshal(schemaData, &schema); err != nil {
26+
t.Fatalf("Failed to parse schema JSON: %v", err)
27+
}
28+
29+
// Navigate to the $ref field
30+
servers, ok := schema["properties"].(map[string]interface{})["data"].(map[string]interface{})["properties"].(map[string]interface{})["servers"].(map[string]interface{})
31+
if !ok {
32+
t.Fatal("Failed to navigate to servers field in schema")
33+
}
34+
35+
items, ok := servers["items"].(map[string]interface{})
36+
if !ok {
37+
t.Fatal("Failed to get items field from servers")
38+
}
39+
40+
refURL, ok := items["$ref"].(string)
41+
if !ok {
42+
t.Fatal("Failed to get $ref URL from items")
43+
}
44+
45+
// Extract the date from the URL
46+
// Expected format: https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json
47+
re := regexp.MustCompile(`/schemas/([0-9]{4}-[0-9]{2}-[0-9]{2})/`)
48+
matches := re.FindStringSubmatch(refURL)
49+
if len(matches) != 2 {
50+
t.Fatalf("Failed to extract date from schema URL: %s", refURL)
51+
}
52+
schemaDate := matches[1]
53+
54+
// Compare with the Go package constant
55+
expectedDate := model.CurrentSchemaVersion
56+
if schemaDate != expectedDate {
57+
t.Errorf("Schema version mismatch!\n"+
58+
" Schema file (%s): %s\n"+
59+
" Go package (model.CurrentSchemaVersion): %s\n\n"+
60+
"To fix: Update schemas/registry.schema.json line 49 to use date %s:\n"+
61+
" \"$ref\": \"https://static.modelcontextprotocol.io/schemas/%s/server.schema.json\"",
62+
schemaPath, schemaDate, expectedDate, expectedDate, expectedDate)
63+
}
64+
65+
// Also check the _schema_version metadata for documentation
66+
schemaVersionMeta, ok := schema["_schema_version"].(map[string]interface{})
67+
if !ok {
68+
t.Log("Warning: _schema_version metadata not found (non-critical)")
69+
return
70+
}
71+
72+
metaDate, ok := schemaVersionMeta["schema_date"].(string)
73+
if ok && metaDate != expectedDate {
74+
t.Errorf("Schema version metadata is out of sync!\n"+
75+
" _schema_version.schema_date: %s\n"+
76+
" Expected: %s\n\n"+
77+
"Update the _schema_version.schema_date field in schemas/registry.schema.json",
78+
metaDate, expectedDate)
79+
}
80+
}

β€Žrenovate.jsonβ€Ž

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,10 @@
3434
"automerge": true
3535
},
3636
{
37-
"description": "MCP Registry dependency updates with automated schema sync",
37+
"description": "MCP Registry dependency updates",
3838
"matchPackageNames": [
3939
"github.com/modelcontextprotocol/registry"
4040
],
41-
"postUpgradeTasks": {
42-
"commands": [
43-
"chmod +x scripts/sync-schema-version.sh",
44-
"./scripts/sync-schema-version.sh"
45-
],
46-
"fileFilters": [
47-
"schemas/registry.schema.json"
48-
],
49-
"executionMode": "update"
50-
},
51-
"prBodyTemplate": "## πŸ”„ MCP Registry Dependency Update\n\nThis PR updates the MCP registry dependency and automatically syncs the schema reference.\n\n### Changes\n- **Dependency**: `github.com/modelcontextprotocol/registry` `{{currentVersion}}` β†’ `{{newVersion}}`\n- **Schema**: Automatically synced schema reference commit SHA to match the updated dependency\n\n### βœ… Automated Tasks Completed\n- βœ… **Go Modules**: Updated and tidied\n- βœ… **Schema Sync**: Schema reference updated to match dependency version\n\n### πŸš€ Next Steps\nThis PR will be validated by the CI workflow:\n- **Registry Validation**: All registry entries will be validated against the new schema\n- **Registry Build**: Both ToolHive and Official MCP formats will be built and tested\n- **Tool Updates**: Any affected registry spec files will be automatically updated\n\nIf validation passes, this PR can be merged immediately. If validation fails, the PR will be converted to draft status with detailed error information.\n\n---\n\nπŸ€– This PR was created automatically by Renovate with enhanced MCP registry support.\n\n{{{links}}}\n\n{{{controls}}}",
5241
"commitMessageTopic": "MCP registry dependency",
5342
"semanticCommitType": "chore",
5443
"semanticCommitScope": "deps"

β€Žschemas/registry.schema.jsonβ€Ž

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"type": "array",
4747
"description": "Array of MCP servers using the official MCP server schema",
4848
"items": {
49-
"$ref": "https://raw.githubusercontent.com/modelcontextprotocol/registry/f975e68cf25c776160d4e837919884ca026027d6/docs/reference/server-json/server.schema.json#/$defs/ServerDetail"
49+
"$ref": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json"
5050
}
5151
},
5252
"groups": {
@@ -61,9 +61,9 @@
6161
}
6262
},
6363
"_schema_version": {
64-
"mcp_registry_version": "v1.0.0",
65-
"mcp_registry_commit": "f975e68cf25c776160d4e837919884ca026027d6",
66-
"updated_at": "2025-09-11T14:07:36Z",
67-
"updated_by": "sync-schema-version.sh"
64+
"mcp_registry_version": "v1.3.3",
65+
"schema_date": "2025-10-17",
66+
"updated_at": "2025-10-22T12:35:00Z",
67+
"updated_by": "manual update to use static.modelcontextprotocol.io schema URL"
6868
}
6969
}

0 commit comments

Comments
Β (0)