diff --git a/cmd/publisher/commands/init.go b/cmd/publisher/commands/init.go index 9c5db75..6b2ac27 100644 --- a/cmd/publisher/commands/init.go +++ b/cmd/publisher/commands/init.go @@ -301,7 +301,7 @@ func createServerJSON( // Create server structure return apiv0.ServerJSON{ - Schema: "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", + Schema: model.CurrentSchemaURL, Name: name, Description: description, Repository: model.Repository{ diff --git a/cmd/publisher/commands/publish.go b/cmd/publisher/commands/publish.go index 0f6b959..6a42c94 100644 --- a/cmd/publisher/commands/publish.go +++ b/cmd/publisher/commands/publish.go @@ -13,6 +13,7 @@ import ( "strings" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" ) func PublishCommand(args []string) error { @@ -39,8 +40,8 @@ func PublishCommand(args []string) error { // Check for deprecated schema and recommend migration // Allow empty schema (will use default) but reject old schemas - if serverJSON.Schema != "" && !strings.Contains(serverJSON.Schema, "2025-09-29") { - return fmt.Errorf(`deprecated schema detected :%s. + if serverJSON.Schema != "" && !strings.Contains(serverJSON.Schema, model.CurrentSchemaVersion) { + return fmt.Errorf(`deprecated schema detected: %s. Migrate to the current schema format for new servers. diff --git a/docs/reference/server-json/generic-server-json.md b/docs/reference/server-json/generic-server-json.md index 549ce79..a5deac0 100644 --- a/docs/reference/server-json/generic-server-json.md +++ b/docs/reference/server-json/generic-server-json.md @@ -106,6 +106,7 @@ Suppose your MCP server application requires a `mcp start` CLI arguments to star ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.joelverhagen/knapcode-samplemcpserver", "description": "Sample NuGet MCP server for a random number and random weather", "version": "0.4.0-beta", @@ -250,6 +251,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol.anonymous/mcp-fs", "description": "Cloud-hosted MCP filesystem server", "repository": { @@ -283,6 +285,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.example/weather-mcp", "description": "Python MCP server for weather data access", "repository": { @@ -337,6 +340,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6. ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.joelverhagen/knapcode-samplemcpserver", "description": "Sample NuGet MCP server for a random number and random weather", "repository": { @@ -385,6 +389,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6. ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.example/database-manager", "description": "MCP server for database operations with support for multiple database types", "repository": { @@ -580,6 +585,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6. ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol/text-editor", "description": "MCP Bundle server for advanced text editing capabilities", "repository": { diff --git a/internal/api/handlers/v0/edit_test.go b/internal/api/handlers/v0/edit_test.go index db614e1..59779c9 100644 --- a/internal/api/handlers/v0/edit_test.go +++ b/internal/api/handlers/v0/edit_test.go @@ -42,6 +42,7 @@ func TestEditServerEndpoint(t *testing.T) { // Create test servers for different scenarios testServers := map[string]*apiv0.ServerJSON{ "editable": { + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/editable-server", Description: "Server that can be edited", Version: "1.0.0", @@ -52,6 +53,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, "other": { + Schema: model.CurrentSchemaURL, Name: "io.github.otheruser/other-server", Description: "Server owned by another user", Version: "1.0.0", @@ -71,6 +73,7 @@ func TestEditServerEndpoint(t *testing.T) { // Create a deleted server for undelete testing deletedServer := &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/deleted-server", Description: "Server that was deleted", Version: "1.0.0", @@ -89,6 +92,7 @@ func TestEditServerEndpoint(t *testing.T) { // Create a server with build metadata for URL encoding test buildMetadataServer := &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/build-metadata-server", Description: "Server with build metadata version", Version: "1.0.0+20130313144700", @@ -125,6 +129,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/editable-server", Description: "Updated server description", Version: "1.0.0", @@ -155,6 +160,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/editable-server", Description: "Server with status change", Version: "1.0.0", @@ -182,6 +188,7 @@ func TestEditServerEndpoint(t *testing.T) { version: "1.0.0", authHeader: "InvalidFormat token123", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/editable-server", Description: "Test server", Version: "1.0.0", @@ -195,6 +202,7 @@ func TestEditServerEndpoint(t *testing.T) { version: "1.0.0", authHeader: "Bearer invalid-token", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/editable-server", Description: "Test server", Version: "1.0.0", @@ -214,6 +222,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/editable-server", Description: "Updated test server", Version: "1.0.0", @@ -233,6 +242,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.otheruser/other-server", Description: "Updated test server", Version: "1.0.0", @@ -252,6 +262,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/non-existent", Description: "Non-existent server", Version: "1.0.0", @@ -271,6 +282,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/renamed-server", // Different name Description: "Trying to rename server", Version: "1.0.0", @@ -290,6 +302,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/editable-server", Description: "Version mismatch test", Version: "2.0.0", // Different version from URL @@ -309,6 +322,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/deleted-server", Description: "Trying to undelete server", Version: "1.0.0", @@ -329,6 +343,7 @@ func TestEditServerEndpoint(t *testing.T) { }, }, requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/build-metadata-server", Description: "Updated server with build metadata", Version: "1.0.0+20130313144700", @@ -432,6 +447,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) { for _, server := range testServers { _, err := registryService.CreateServer(context.Background(), &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: server.name, Description: "Test server for editing", Version: server.version, @@ -441,6 +457,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) { // Set specific status if not active if server.status != model.StatusActive { _, err = registryService.UpdateServer(context.Background(), server.name, server.version, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: server.name, Description: "Test server for editing", Version: server.version, @@ -498,6 +515,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { requestBody := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: tt.serverName, Description: "Status transition test", Version: tt.version, @@ -546,6 +564,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) { // Create server with special characters specialServerName := "io.dots.and-dashes/server_with_underscores" _, err := registryService.CreateServer(context.Background(), &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: specialServerName, Description: "Server with special characters", Version: "1.0.0", @@ -553,6 +572,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) { require.NoError(t, err) requestBody := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: specialServerName, Description: "Updated server with special chars", Version: "1.0.0", @@ -593,6 +613,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) { t.Run("version-specific editing", func(t *testing.T) { // Test editing a specific version of a multi-version server requestBody := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/multi-version-server", Description: "Updated v1.0.0 specifically", Version: "1.0.0", diff --git a/internal/api/handlers/v0/publish_integration_test.go b/internal/api/handlers/v0/publish_integration_test.go index 1bd6a19..a8d317a 100644 --- a/internal/api/handlers/v0/publish_integration_test.go +++ b/internal/api/handlers/v0/publish_integration_test.go @@ -57,6 +57,7 @@ func TestPublishIntegration(t *testing.T) { t.Run("successful publish with GitHub auth", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.testuser/test-mcp-server", Description: "A test MCP server for integration testing", Repository: model.Repository{ @@ -101,6 +102,7 @@ func TestPublishIntegration(t *testing.T) { t.Run("successful publish with none auth (no prefix)", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-mcp-server-no-auth", Description: "A test MCP server without authentication", Repository: model.Repository{ @@ -143,7 +145,8 @@ func TestPublishIntegration(t *testing.T) { t.Run("publish fails with missing authorization header", func(t *testing.T) { publishReq := apiv0.ServerJSON{ - Name: "test-server", + Schema: model.CurrentSchemaURL, + Name: "test-server", } body, err := json.Marshal(publishReq) @@ -162,6 +165,7 @@ func TestPublishIntegration(t *testing.T) { t.Run("publish fails with invalid token", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.domdomegg/test-server", Description: "Test server", Version: "1.0.0", @@ -183,6 +187,7 @@ func TestPublishIntegration(t *testing.T) { t.Run("publish fails when permission denied", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.other/test-server", Description: "A test server", Version: "1.0.0", @@ -219,6 +224,7 @@ func TestPublishIntegration(t *testing.T) { t.Run("publish succeeds with MCPB package", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.domdomegg/airtable-mcp-server", Description: "A test server with MCPB package", Version: "1.7.2", diff --git a/internal/api/handlers/v0/publish_registry_validation_test.go b/internal/api/handlers/v0/publish_registry_validation_test.go index 11ff938..3529630 100644 --- a/internal/api/handlers/v0/publish_registry_validation_test.go +++ b/internal/api/handlers/v0/publish_registry_validation_test.go @@ -45,6 +45,7 @@ func TestPublishRegistryValidation(t *testing.T) { t.Run("publish fails with npm registry validation error", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server-with-npm", Description: "A test server with invalid npm package reference", Version: "1.0.0", @@ -86,6 +87,7 @@ func TestPublishRegistryValidation(t *testing.T) { t.Run("publish succeeds with MCPB package (registry validation enabled)", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server-mcpb-validation", Description: "A test server with MCPB package and registry validation enabled", Version: "0.0.36", @@ -136,6 +138,7 @@ func TestPublishRegistryValidation(t *testing.T) { t.Run("publish fails when second package fails npm validation", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server-multiple-packages", Description: "A test server with multiple packages where second fails", Version: "1.0.0", @@ -187,6 +190,7 @@ func TestPublishRegistryValidation(t *testing.T) { t.Run("publish fails when first package fails validation", func(t *testing.T) { publishReq := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server-first-package-fails", Description: "A test server where first package fails", Version: "1.0.0", diff --git a/internal/api/handlers/v0/publish_test.go b/internal/api/handlers/v0/publish_test.go index f64c0d8..f04510e 100644 --- a/internal/api/handlers/v0/publish_test.go +++ b/internal/api/handlers/v0/publish_test.go @@ -56,6 +56,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "successful publish with GitHub auth", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -80,6 +81,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "successful publish with no auth (AuthMethodNone)", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "example/test-server", Description: "A test server without auth", Repository: model.Repository{ @@ -113,6 +115,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid authorization header format", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.domdomegg/test-server", Description: "Test server", Version: "1.0.0", @@ -127,6 +130,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid token", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "test-server", Description: "A test server", Version: "1.0.0", @@ -141,6 +145,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "permission denied", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.other/test-server", Description: "A test server", Version: "1.0.0", @@ -165,6 +170,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "registry service error", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "example/test-server", Description: "A test server", Version: "1.0.0", @@ -183,6 +189,7 @@ func TestPublishEndpoint(t *testing.T) { setupRegistryService: func(registry service.RegistryService) { // Pre-publish the same server to cause duplicate version error existingServer := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "example/test-server", Description: "Existing test server", Version: "1.0.0", @@ -200,6 +207,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "package validation success - MCPB package", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server-mcpb", Description: "A test server with MCPB package", Version: "1.0.0", @@ -227,6 +235,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid server name - multiple slashes (two slashes)", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/server/path", Description: "Server with multiple slashes in name", Version: "1.0.0", @@ -249,6 +258,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid server name - multiple slashes (three slashes)", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "org.company/dept/team/project", Description: "Server with three slashes in name", Version: "1.0.0", @@ -266,6 +276,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid server name - consecutive slashes", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example//double-slash", Description: "Server with consecutive slashes", Version: "1.0.0", @@ -283,6 +294,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid server name - URL-like path", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/servers/v1/api", Description: "Server with URL-like path structure", Version: "1.0.0", @@ -300,6 +312,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid server name - many slashes", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "a/b/c/d/e/f", Description: "Server with many slashes", Version: "1.0.0", @@ -317,6 +330,7 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid server name - with packages and remotes", requestBody: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test/server/v2", Description: "Complex server with invalid name", Version: "2.0.0", @@ -470,6 +484,7 @@ func TestPublishEndpoint_MultipleSlashesEdgeCases(t *testing.T) { // Create request body requestBody := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: tc.serverName, Description: "Test server", Version: "1.0.0", diff --git a/internal/api/handlers/v0/servers_test.go b/internal/api/handlers/v0/servers_test.go index c281359..d7e379f 100644 --- a/internal/api/handlers/v0/servers_test.go +++ b/internal/api/handlers/v0/servers_test.go @@ -26,6 +26,7 @@ func TestListServersEndpoint(t *testing.T) { // Setup test data _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/server-alpha", Description: "Alpha test server", Version: "1.0.0", @@ -33,6 +34,7 @@ func TestListServersEndpoint(t *testing.T) { require.NoError(t, err) _, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/server-beta", Description: "Beta test server", Version: "2.0.0", @@ -118,6 +120,7 @@ func TestGetServerByNameEndpoint(t *testing.T) { // Setup test data _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/detail-server", Description: "Server for detail testing", Version: "1.0.0", @@ -180,6 +183,7 @@ func TestGetServerVersionEndpoint(t *testing.T) { // Setup test data with multiple versions _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Version test server v1", Version: "1.0.0", @@ -187,6 +191,7 @@ func TestGetServerVersionEndpoint(t *testing.T) { require.NoError(t, err) _, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Version test server v2", Version: "2.0.0", @@ -195,6 +200,7 @@ func TestGetServerVersionEndpoint(t *testing.T) { // Add version with build metadata for URL encoding test _, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Version test server with build metadata", Version: "1.0.0+20130313144700", @@ -304,6 +310,7 @@ func TestGetAllVersionsEndpoint(t *testing.T) { versions := []string{"1.0.0", "1.1.0", "2.0.0"} for _, version := range versions { _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Multi-version test server " + version, Version: version, @@ -402,6 +409,7 @@ func TestServersEndpointEdgeCases(t *testing.T) { for _, server := range specialServers { _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: server.name, Description: server.description, Version: server.version, diff --git a/internal/api/handlers/v0/telemetry_test.go b/internal/api/handlers/v0/telemetry_test.go index 2cffce1..1e797d6 100644 --- a/internal/api/handlers/v0/telemetry_test.go +++ b/internal/api/handlers/v0/telemetry_test.go @@ -24,6 +24,7 @@ import ( func TestPrometheusHandler(t *testing.T) { registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) server, err := registryService.CreateServer(context.Background(), &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "io.github.example/test-server", Description: "Test server detail", Repository: model.Repository{ diff --git a/internal/importer/importer_test.go b/internal/importer/importer_test.go index f7e8a2b..0a4f7b6 100644 --- a/internal/importer/importer_test.go +++ b/internal/importer/importer_test.go @@ -23,6 +23,7 @@ func TestImportService_LocalFile(t *testing.T) { tempFile := "/tmp/test_import_seed.json" seedData := []*apiv0.ServerJSON{ { + Schema: model.CurrentSchemaURL, Name: "io.github.test/test-server-1", Description: "Test server 1", Repository: model.Repository{ @@ -65,6 +66,7 @@ func TestImportService_HTTPFile(t *testing.T) { // Create a test HTTP server seedData := []*apiv0.ServerJSON{ { + Schema: model.CurrentSchemaURL, Name: "io.github.test/http-test-server", Description: "HTTP test server", Repository: model.Repository{ @@ -115,11 +117,13 @@ func TestImportService_RegistryPagination(t *testing.T) { // Setup source registry with test data sourceServers := []*apiv0.ServerJSON{ { + Schema: model.CurrentSchemaURL, Name: "com.source/server-1", Description: "Source server 1", Version: "1.0.0", }, { + Schema: model.CurrentSchemaURL, Name: "com.source/server-2", Description: "Source server 2", Version: "1.0.0", diff --git a/internal/service/registry_service_test.go b/internal/service/registry_service_test.go index 6866361..b091e1e 100644 --- a/internal/service/registry_service_test.go +++ b/internal/service/registry_service_test.go @@ -22,6 +22,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { // Create test data existingServers := map[string]*apiv0.ServerJSON{ "existing1": { + Schema: model.CurrentSchemaURL, Name: "com.example/existing-server", Description: "An existing server", Version: "1.0.0", @@ -31,6 +32,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { }, }, "existing2": { + Schema: model.CurrentSchemaURL, Name: "com.microsoft/another-server", Description: "Another existing server", Version: "1.0.0", @@ -58,6 +60,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { { name: "no remote URLs - should pass", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/new-server", Description: "A new server with no remotes", Version: "1.0.0", @@ -68,6 +71,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { { name: "new unique remote URLs - should pass", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/new-server-unique", Description: "A new server", Version: "1.0.0", @@ -81,6 +85,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { { name: "duplicate remote URL - should fail", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/new-server-duplicate", Description: "A new server with duplicate URL", Version: "1.0.0", @@ -94,6 +99,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { { name: "updating same server with same URLs - should pass", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/existing-server", // Same name as existing Description: "Updated existing server", Version: "1.1.0", // Different version @@ -128,6 +134,7 @@ func TestGetServerByName(t *testing.T) { // Create multiple versions of the same server _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "Test server v1", Version: "1.0.0", @@ -135,6 +142,7 @@ func TestGetServerByName(t *testing.T) { require.NoError(t, err) _, err = service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "Test server v2", Version: "2.0.0", @@ -197,6 +205,7 @@ func TestGetServerByNameAndVersion(t *testing.T) { // Create multiple versions of the same server _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Versioned server v1", Version: "1.0.0", @@ -204,6 +213,7 @@ func TestGetServerByNameAndVersion(t *testing.T) { require.NoError(t, err) _, err = service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Versioned server v2", Version: "2.0.0", @@ -288,6 +298,7 @@ func TestGetAllVersionsByServerName(t *testing.T) { // Create multiple versions of the same server _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Multi-version server v1", Version: "1.0.0", @@ -295,6 +306,7 @@ func TestGetAllVersionsByServerName(t *testing.T) { require.NoError(t, err) _, err = service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Multi-version server v2", Version: "2.0.0", @@ -302,6 +314,7 @@ func TestGetAllVersionsByServerName(t *testing.T) { require.NoError(t, err) _, err = service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Multi-version server v2.1", Version: "2.1.0", @@ -388,6 +401,7 @@ func TestCreateServerConcurrentVersionsNoRace(t *testing.T) { go func(idx int) { defer wg.Done() result, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: fmt.Sprintf("Version %d", idx), Version: fmt.Sprintf("1.0.%d", idx), @@ -437,6 +451,7 @@ func TestUpdateServer(t *testing.T) { // Create initial server _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Original description", Version: version, @@ -461,6 +476,7 @@ func TestUpdateServer(t *testing.T) { serverName: serverName, version: version, updatedServer: &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Updated description", Version: version, @@ -482,6 +498,7 @@ func TestUpdateServer(t *testing.T) { serverName: serverName, version: version, updatedServer: &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Updated with status change", Version: version, @@ -499,6 +516,7 @@ func TestUpdateServer(t *testing.T) { serverName: "com.example/non-existent", version: "1.0.0", updatedServer: &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/non-existent", Description: "Should fail", Version: "1.0.0", @@ -540,6 +558,7 @@ func TestUpdateServer_SkipValidationForDeletedServers(t *testing.T) { // Create server with invalid package configuration (this would fail registry validation) invalidServer := &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Server with invalid package for testing validation skip", Version: version, @@ -572,6 +591,7 @@ func TestUpdateServer_SkipValidationForDeletedServers(t *testing.T) { // Now try to update a deleted server - validation should be skipped updatedInvalidServer := &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: "Updated description for deleted server", Version: version, @@ -594,6 +614,7 @@ func TestUpdateServer_SkipValidationForDeletedServers(t *testing.T) { // Test updating a server being set to deleted status activeServer := &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/being-deleted-test", Description: "Server being deleted", Version: "1.0.0", @@ -639,6 +660,7 @@ func TestListServers(t *testing.T) { for _, server := range testServers { _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: server.name, Description: server.description, Version: server.version, @@ -736,6 +758,7 @@ func TestVersionComparison(t *testing.T) { time.Sleep(v.delay) } _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: serverName, Description: v.description, Version: v.version, diff --git a/internal/validators/validators.go b/internal/validators/validators.go index 25cdf32..8ceb309 100644 --- a/internal/validators/validators.go +++ b/internal/validators/validators.go @@ -53,6 +53,16 @@ var ( ) func ValidateServerJSON(serverJSON *apiv0.ServerJSON) error { + // Validate schema version is provided and supported + // Note: Schema field is also marked as required in the ServerJSON struct definition + // for API-level validation and documentation + if serverJSON.Schema == "" { + return fmt.Errorf("$schema field is required") + } + if !strings.Contains(serverJSON.Schema, model.CurrentSchemaVersion) { + return fmt.Errorf("schema version %s is not supported. Please use schema version %s", serverJSON.Schema, model.CurrentSchemaVersion) + } + // Validate server name exists and format if _, err := parseServerName(*serverJSON); err != nil { return err diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index 0bdc508..01276c2 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -19,9 +19,51 @@ func TestValidate(t *testing.T) { serverDetail apiv0.ServerJSON expectedError string }{ + { + name: "Schema version is required", + serverDetail: apiv0.ServerJSON{ + Name: "com.example/test-server", + Description: "A test server", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "$schema field is required", + }, + { + name: "Schema version rejects old schema (2025-01-27)", + serverDetail: apiv0.ServerJSON{ + Schema: "https://static.modelcontextprotocol.io/schemas/2025-01-27/server.schema.json", + Name: "com.example/test-server", + Description: "A test server", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "schema version https://static.modelcontextprotocol.io/schemas/2025-01-27/server.schema.json is not supported", + }, + { + name: "Schema version accepts current schema (2025-09-29)", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, { name: "Version rejects top-level version ranges", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -35,6 +77,7 @@ func TestValidate(t *testing.T) { { name: "rejects package version ranges", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -56,6 +99,7 @@ func TestValidate(t *testing.T) { { name: "Version allows specific versions (semver)", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -77,6 +121,7 @@ func TestValidate(t *testing.T) { { name: "Version allows specific non-semver versions", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -98,6 +143,7 @@ func TestValidate(t *testing.T) { { name: "Version rejects wildcard and x-range", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -111,6 +157,7 @@ func TestValidate(t *testing.T) { { name: "Version rejects wildcard *", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -124,6 +171,7 @@ func TestValidate(t *testing.T) { { name: "Version allows freeform version with hyphen not a range", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -137,6 +185,7 @@ func TestValidate(t *testing.T) { { name: "Version rejects hyphen range of two versions", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -150,6 +199,7 @@ func TestValidate(t *testing.T) { { name: "Version rejects OR range with two versions", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -163,6 +213,7 @@ func TestValidate(t *testing.T) { { name: "Version rejects comparator with space", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -177,6 +228,7 @@ func TestValidate(t *testing.T) { { name: "server name with two slashes", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/server/path", Description: "A test server", Version: "1.0.0", @@ -186,6 +238,7 @@ func TestValidate(t *testing.T) { { name: "server name with three slashes", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/server/path/deep", Description: "A test server", Version: "1.0.0", @@ -195,6 +248,7 @@ func TestValidate(t *testing.T) { { name: "valid server detail with all fields", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -226,6 +280,7 @@ func TestValidate(t *testing.T) { { name: "server with invalid repository source", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -239,6 +294,7 @@ func TestValidate(t *testing.T) { { name: "server with invalid GitHub URL format", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -252,6 +308,7 @@ func TestValidate(t *testing.T) { { name: "server with invalid GitLab URL format", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -265,6 +322,7 @@ func TestValidate(t *testing.T) { { name: "server with valid repository subfolder", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -279,6 +337,7 @@ func TestValidate(t *testing.T) { { name: "server with repository subfolder containing path traversal", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -293,6 +352,7 @@ func TestValidate(t *testing.T) { { name: "server with repository subfolder starting with slash", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -307,6 +367,7 @@ func TestValidate(t *testing.T) { { name: "server with repository subfolder ending with slash", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -321,6 +382,7 @@ func TestValidate(t *testing.T) { { name: "server with repository subfolder containing invalid characters", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -335,6 +397,7 @@ func TestValidate(t *testing.T) { { name: "server with repository subfolder containing empty segments", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -349,6 +412,7 @@ func TestValidate(t *testing.T) { { name: "server with valid websiteUrl", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -363,6 +427,7 @@ func TestValidate(t *testing.T) { { name: "server with invalid websiteUrl - no scheme", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -377,6 +442,7 @@ func TestValidate(t *testing.T) { { name: "server with invalid websiteUrl - invalid scheme", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -391,6 +457,7 @@ func TestValidate(t *testing.T) { { name: "server with malformed websiteUrl", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -405,6 +472,7 @@ func TestValidate(t *testing.T) { { name: "server with websiteUrl that matches namespace domain", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -419,6 +487,7 @@ func TestValidate(t *testing.T) { { name: "server with websiteUrl subdomain that matches namespace", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -433,6 +502,7 @@ func TestValidate(t *testing.T) { { name: "server with websiteUrl that does not match namespace", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -447,6 +517,7 @@ func TestValidate(t *testing.T) { { name: "package with spaces in name", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -470,6 +541,7 @@ func TestValidate(t *testing.T) { { name: "package with reserved version 'latest'", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -494,6 +566,7 @@ func TestValidate(t *testing.T) { { name: "multiple packages with one invalid", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -525,6 +598,7 @@ func TestValidate(t *testing.T) { { name: "remote with invalid URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -544,6 +618,7 @@ func TestValidate(t *testing.T) { { name: "remote with missing scheme", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -563,6 +638,7 @@ func TestValidate(t *testing.T) { { name: "remote with localhost url", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -582,6 +658,7 @@ func TestValidate(t *testing.T) { { name: "remote with localhost url with port", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -601,6 +678,7 @@ func TestValidate(t *testing.T) { { name: "multiple remotes with one invalid", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -624,6 +702,7 @@ func TestValidate(t *testing.T) { { name: "server detail with nil packages and remotes", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -639,6 +718,7 @@ func TestValidate(t *testing.T) { { name: "server detail with empty packages and remotes slices", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ @@ -677,7 +757,8 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "valid match - example.com domain", serverDetail: apiv0.ServerJSON{ - Name: "com.example/test-server", + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", Remotes: []model.Transport{ { Type: "streamable-http", @@ -690,7 +771,8 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "valid match - subdomain mcp.example.com", serverDetail: apiv0.ServerJSON{ - Name: "com.example/test-server", + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", Remotes: []model.Transport{ { Type: "streamable-http", @@ -703,7 +785,8 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "valid match - api subdomain", serverDetail: apiv0.ServerJSON{ - Name: "com.example/api-server", + Schema: model.CurrentSchemaURL, + Name: "com.example/api-server", Remotes: []model.Transport{ { Type: "streamable-http", @@ -716,7 +799,8 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "invalid - wrong domain", serverDetail: apiv0.ServerJSON{ - Name: "com.example/test-server", + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", Remotes: []model.Transport{ { Type: "streamable-http", @@ -730,7 +814,8 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "invalid - different domain entirely", serverDetail: apiv0.ServerJSON{ - Name: "com.microsoft/server", + Schema: model.CurrentSchemaURL, + Name: "com.microsoft/server", Remotes: []model.Transport{ { Type: "streamable-http", @@ -744,7 +829,8 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "invalid URL format", serverDetail: apiv0.ServerJSON{ - Name: "com.example/test", + Schema: model.CurrentSchemaURL, + Name: "com.example/test", Remotes: []model.Transport{ { Type: "streamable-http", @@ -758,6 +844,7 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "empty remotes array", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test", Remotes: []model.Transport{}, }, @@ -766,7 +853,8 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "multiple valid remotes - different subdomains", serverDetail: apiv0.ServerJSON{ - Name: "com.example/server", + Schema: model.CurrentSchemaURL, + Name: "com.example/server", Remotes: []model.Transport{ { Type: "streamable-http", @@ -783,7 +871,8 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) { { name: "one valid, one invalid remote", serverDetail: apiv0.ServerJSON{ - Name: "com.example/server", + Schema: model.CurrentSchemaURL, + Name: "com.example/server", Remotes: []model.Transport{ { Type: "streamable-http", @@ -824,21 +913,24 @@ func TestValidate_ServerNameFormat(t *testing.T) { { name: "valid namespace/name format", serverDetail: apiv0.ServerJSON{ - Name: "com.example.api/server", + Schema: model.CurrentSchemaURL, + Name: "com.example.api/server", }, expectError: false, }, { name: "valid complex namespace", serverDetail: apiv0.ServerJSON{ - Name: "com.microsoft.azure.service/webapp-server", + Schema: model.CurrentSchemaURL, + Name: "com.microsoft.azure.service/webapp-server", }, expectError: false, }, { name: "empty server name", serverDetail: apiv0.ServerJSON{ - Name: "", + Schema: model.CurrentSchemaURL, + Name: "", }, expectError: true, errorMsg: "server name is required", @@ -846,7 +938,8 @@ func TestValidate_ServerNameFormat(t *testing.T) { { name: "missing slash separator", serverDetail: apiv0.ServerJSON{ - Name: "com.example.server", + Schema: model.CurrentSchemaURL, + Name: "com.example.server", }, expectError: true, errorMsg: "server name must be in format 'dns-namespace/name'", @@ -854,7 +947,8 @@ func TestValidate_ServerNameFormat(t *testing.T) { { name: "empty namespace part", serverDetail: apiv0.ServerJSON{ - Name: "/server-name", + Schema: model.CurrentSchemaURL, + Name: "/server-name", }, expectError: true, errorMsg: "non-empty namespace and name parts", @@ -862,7 +956,8 @@ func TestValidate_ServerNameFormat(t *testing.T) { { name: "empty name part", serverDetail: apiv0.ServerJSON{ - Name: "com.example/", + Schema: model.CurrentSchemaURL, + Name: "com.example/", }, expectError: true, errorMsg: "non-empty namespace and name parts", @@ -870,7 +965,8 @@ func TestValidate_ServerNameFormat(t *testing.T) { { name: "multiple slashes - should be rejected", serverDetail: apiv0.ServerJSON{ - Name: "com.example/server/path", + Schema: model.CurrentSchemaURL, + Name: "com.example/server/path", }, expectError: true, errorMsg: "server name cannot contain multiple slashes", @@ -955,7 +1051,8 @@ func TestValidate_MultipleSlashesInServerName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { serverDetail := apiv0.ServerJSON{ - Name: tt.serverName, + Schema: model.CurrentSchemaURL, + Name: tt.serverName, } err := validators.ValidateServerJSON(&serverDetail) @@ -1173,6 +1270,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport stdio without URL should pass", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1191,6 +1289,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport stdio with URL (should fail)", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1211,6 +1310,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport streamable-http with valid URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1230,6 +1330,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport streamable-http with templated URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1253,6 +1354,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport streamable-http without URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1271,6 +1373,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport streamable-http with templated URL missing variables", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1292,6 +1395,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport sse with valid URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1311,6 +1415,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport sse without URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1330,6 +1435,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport unsupported type", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1349,6 +1455,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "remote transport streamable-http with valid URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1364,6 +1471,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "remote transport streamable-http without URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1379,6 +1487,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "remote transport sse with valid URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1394,6 +1503,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "remote transport sse without URL", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1409,6 +1519,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "remote transport stdio not supported", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1423,6 +1534,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "remote transport unsupported type", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1439,6 +1551,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "package transport allows localhost URLs", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1458,6 +1571,7 @@ func TestValidate_TransportValidation(t *testing.T) { { name: "remote transport rejects localhost URLs", serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", @@ -1530,6 +1644,7 @@ func TestValidate_RegistryTypesAndUrls(t *testing.T) { for _, tc := range testCases { t.Run(tc.tcName, func(t *testing.T) { serverJSON := apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: tc.name, Description: "A test server", Repository: model.Repository{ @@ -1566,6 +1681,7 @@ func TestValidate_RegistryTypesAndUrls(t *testing.T) { func createValidServerWithArgument(arg model.Argument) apiv0.ServerJSON { return apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, Name: "com.example/test-server", Description: "A test server", Repository: model.Repository{ diff --git a/pkg/api/v0/types.go b/pkg/api/v0/types.go index 73d3e33..859ec78 100644 --- a/pkg/api/v0/types.go +++ b/pkg/api/v0/types.go @@ -38,7 +38,7 @@ type ServerMeta struct { // ServerJSON represents complete server information as defined in the MCP spec, with extension support type ServerJSON struct { - Schema string `json:"$schema,omitempty"` + Schema string `json:"$schema" required:"true" minLength:"1"` Name string `json:"name" minLength:"1" maxLength:"200"` Description string `json:"description" minLength:"1" maxLength:"100"` Repository model.Repository `json:"repository,omitempty"` diff --git a/pkg/model/constants.go b/pkg/model/constants.go index f21f172..dce2750 100644 --- a/pkg/model/constants.go +++ b/pkg/model/constants.go @@ -34,3 +34,11 @@ const ( RuntimeHintDocker = "docker" RuntimeHintDNX = "dnx" ) + +// Schema versions +const ( + // CurrentSchemaVersion is the current supported schema version date + CurrentSchemaVersion = "2025-09-29" + // CurrentSchemaURL is the full URL to the current schema + CurrentSchemaURL = "https://static.modelcontextprotocol.io/schemas/" + CurrentSchemaVersion + "/server.schema.json" +)