Enforce schema validation on the publish and edit API level (#598)

<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
The following PR:
* Enforces schema validation on the API level (requests without schema
or older schema will fail)
* Updates the unit tests and examples to match that

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

## Breaking Changes
<!-- Will users need to update their code or configurations? -->
**Note this can be considered breaking if clients have not included it
before.**

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

---------

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
This commit is contained in:
Radoslav Dimitrov 2025-10-05 15:57:10 +03:00 • committed by GitHub
commit 44950c23bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 244 additions and 21 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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,

View file

@ -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{