Skip to content

Commit 44950c2

Browse files
authored
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 <[email protected]>
1 parent e0f30f1 commit 44950c2

File tree

15 files changed

+244
-21
lines changed

15 files changed

+244
-21
lines changed

cmd/publisher/commands/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ func createServerJSON(
301301

302302
// Create server structure
303303
return apiv0.ServerJSON{
304-
Schema: "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
304+
Schema: model.CurrentSchemaURL,
305305
Name: name,
306306
Description: description,
307307
Repository: model.Repository{

cmd/publisher/commands/publish.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strings"
1414

1515
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
16+
"github.com/modelcontextprotocol/registry/pkg/model"
1617
)
1718

1819
func PublishCommand(args []string) error {
@@ -39,8 +40,8 @@ func PublishCommand(args []string) error {
3940

4041
// Check for deprecated schema and recommend migration
4142
// Allow empty schema (will use default) but reject old schemas
42-
if serverJSON.Schema != "" && !strings.Contains(serverJSON.Schema, "2025-09-29") {
43-
return fmt.Errorf(`deprecated schema detected :%s.
43+
if serverJSON.Schema != "" && !strings.Contains(serverJSON.Schema, model.CurrentSchemaVersion) {
44+
return fmt.Errorf(`deprecated schema detected: %s.
4445
4546
Migrate to the current schema format for new servers.
4647

docs/reference/server-json/generic-server-json.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Suppose your MCP server application requires a `mcp start` CLI arguments to star
106106

107107
```json
108108
{
109+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
109110
"name": "io.github.joelverhagen/knapcode-samplemcpserver",
110111
"description": "Sample NuGet MCP server for a random number and random weather",
111112
"version": "0.4.0-beta",
@@ -250,6 +251,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp
250251

251252
```json
252253
{
254+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
253255
"name": "io.modelcontextprotocol.anonymous/mcp-fs",
254256
"description": "Cloud-hosted MCP filesystem server",
255257
"repository": {
@@ -283,6 +285,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp
283285

284286
```json
285287
{
288+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
286289
"name": "io.github.example/weather-mcp",
287290
"description": "Python MCP server for weather data access",
288291
"repository": {
@@ -337,6 +340,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6.
337340

338341
```json
339342
{
343+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
340344
"name": "io.github.joelverhagen/knapcode-samplemcpserver",
341345
"description": "Sample NuGet MCP server for a random number and random weather",
342346
"repository": {
@@ -385,6 +389,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6.
385389

386390
```json
387391
{
392+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
388393
"name": "io.github.example/database-manager",
389394
"description": "MCP server for database operations with support for multiple database types",
390395
"repository": {
@@ -580,6 +585,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6.
580585

581586
```json
582587
{
588+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
583589
"name": "io.modelcontextprotocol/text-editor",
584590
"description": "MCP Bundle server for advanced text editing capabilities",
585591
"repository": {

internal/api/handlers/v0/edit_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func TestEditServerEndpoint(t *testing.T) {
4242
// Create test servers for different scenarios
4343
testServers := map[string]*apiv0.ServerJSON{
4444
"editable": {
45+
Schema: model.CurrentSchemaURL,
4546
Name: "io.github.testuser/editable-server",
4647
Description: "Server that can be edited",
4748
Version: "1.0.0",
@@ -52,6 +53,7 @@ func TestEditServerEndpoint(t *testing.T) {
5253
},
5354
},
5455
"other": {
56+
Schema: model.CurrentSchemaURL,
5557
Name: "io.github.otheruser/other-server",
5658
Description: "Server owned by another user",
5759
Version: "1.0.0",
@@ -71,6 +73,7 @@ func TestEditServerEndpoint(t *testing.T) {
7173

7274
// Create a deleted server for undelete testing
7375
deletedServer := &apiv0.ServerJSON{
76+
Schema: model.CurrentSchemaURL,
7477
Name: "io.github.testuser/deleted-server",
7578
Description: "Server that was deleted",
7679
Version: "1.0.0",
@@ -89,6 +92,7 @@ func TestEditServerEndpoint(t *testing.T) {
8992

9093
// Create a server with build metadata for URL encoding test
9194
buildMetadataServer := &apiv0.ServerJSON{
95+
Schema: model.CurrentSchemaURL,
9296
Name: "io.github.testuser/build-metadata-server",
9397
Description: "Server with build metadata version",
9498
Version: "1.0.0+20130313144700",
@@ -125,6 +129,7 @@ func TestEditServerEndpoint(t *testing.T) {
125129
},
126130
},
127131
requestBody: apiv0.ServerJSON{
132+
Schema: model.CurrentSchemaURL,
128133
Name: "io.github.testuser/editable-server",
129134
Description: "Updated server description",
130135
Version: "1.0.0",
@@ -155,6 +160,7 @@ func TestEditServerEndpoint(t *testing.T) {
155160
},
156161
},
157162
requestBody: apiv0.ServerJSON{
163+
Schema: model.CurrentSchemaURL,
158164
Name: "io.github.testuser/editable-server",
159165
Description: "Server with status change",
160166
Version: "1.0.0",
@@ -182,6 +188,7 @@ func TestEditServerEndpoint(t *testing.T) {
182188
version: "1.0.0",
183189
authHeader: "InvalidFormat token123",
184190
requestBody: apiv0.ServerJSON{
191+
Schema: model.CurrentSchemaURL,
185192
Name: "io.github.testuser/editable-server",
186193
Description: "Test server",
187194
Version: "1.0.0",
@@ -195,6 +202,7 @@ func TestEditServerEndpoint(t *testing.T) {
195202
version: "1.0.0",
196203
authHeader: "Bearer invalid-token",
197204
requestBody: apiv0.ServerJSON{
205+
Schema: model.CurrentSchemaURL,
198206
Name: "io.github.testuser/editable-server",
199207
Description: "Test server",
200208
Version: "1.0.0",
@@ -214,6 +222,7 @@ func TestEditServerEndpoint(t *testing.T) {
214222
},
215223
},
216224
requestBody: apiv0.ServerJSON{
225+
Schema: model.CurrentSchemaURL,
217226
Name: "io.github.testuser/editable-server",
218227
Description: "Updated test server",
219228
Version: "1.0.0",
@@ -233,6 +242,7 @@ func TestEditServerEndpoint(t *testing.T) {
233242
},
234243
},
235244
requestBody: apiv0.ServerJSON{
245+
Schema: model.CurrentSchemaURL,
236246
Name: "io.github.otheruser/other-server",
237247
Description: "Updated test server",
238248
Version: "1.0.0",
@@ -252,6 +262,7 @@ func TestEditServerEndpoint(t *testing.T) {
252262
},
253263
},
254264
requestBody: apiv0.ServerJSON{
265+
Schema: model.CurrentSchemaURL,
255266
Name: "io.github.testuser/non-existent",
256267
Description: "Non-existent server",
257268
Version: "1.0.0",
@@ -271,6 +282,7 @@ func TestEditServerEndpoint(t *testing.T) {
271282
},
272283
},
273284
requestBody: apiv0.ServerJSON{
285+
Schema: model.CurrentSchemaURL,
274286
Name: "io.github.testuser/renamed-server", // Different name
275287
Description: "Trying to rename server",
276288
Version: "1.0.0",
@@ -290,6 +302,7 @@ func TestEditServerEndpoint(t *testing.T) {
290302
},
291303
},
292304
requestBody: apiv0.ServerJSON{
305+
Schema: model.CurrentSchemaURL,
293306
Name: "io.github.testuser/editable-server",
294307
Description: "Version mismatch test",
295308
Version: "2.0.0", // Different version from URL
@@ -309,6 +322,7 @@ func TestEditServerEndpoint(t *testing.T) {
309322
},
310323
},
311324
requestBody: apiv0.ServerJSON{
325+
Schema: model.CurrentSchemaURL,
312326
Name: "io.github.testuser/deleted-server",
313327
Description: "Trying to undelete server",
314328
Version: "1.0.0",
@@ -329,6 +343,7 @@ func TestEditServerEndpoint(t *testing.T) {
329343
},
330344
},
331345
requestBody: apiv0.ServerJSON{
346+
Schema: model.CurrentSchemaURL,
332347
Name: "io.github.testuser/build-metadata-server",
333348
Description: "Updated server with build metadata",
334349
Version: "1.0.0+20130313144700",
@@ -432,6 +447,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) {
432447

433448
for _, server := range testServers {
434449
_, err := registryService.CreateServer(context.Background(), &apiv0.ServerJSON{
450+
Schema: model.CurrentSchemaURL,
435451
Name: server.name,
436452
Description: "Test server for editing",
437453
Version: server.version,
@@ -441,6 +457,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) {
441457
// Set specific status if not active
442458
if server.status != model.StatusActive {
443459
_, err = registryService.UpdateServer(context.Background(), server.name, server.version, &apiv0.ServerJSON{
460+
Schema: model.CurrentSchemaURL,
444461
Name: server.name,
445462
Description: "Test server for editing",
446463
Version: server.version,
@@ -498,6 +515,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) {
498515
for _, tt := range tests {
499516
t.Run(tt.name, func(t *testing.T) {
500517
requestBody := apiv0.ServerJSON{
518+
Schema: model.CurrentSchemaURL,
501519
Name: tt.serverName,
502520
Description: "Status transition test",
503521
Version: tt.version,
@@ -546,13 +564,15 @@ func TestEditServerEndpointEdgeCases(t *testing.T) {
546564
// Create server with special characters
547565
specialServerName := "io.dots.and-dashes/server_with_underscores"
548566
_, err := registryService.CreateServer(context.Background(), &apiv0.ServerJSON{
567+
Schema: model.CurrentSchemaURL,
549568
Name: specialServerName,
550569
Description: "Server with special characters",
551570
Version: "1.0.0",
552571
})
553572
require.NoError(t, err)
554573

555574
requestBody := apiv0.ServerJSON{
575+
Schema: model.CurrentSchemaURL,
556576
Name: specialServerName,
557577
Description: "Updated server with special chars",
558578
Version: "1.0.0",
@@ -593,6 +613,7 @@ func TestEditServerEndpointEdgeCases(t *testing.T) {
593613
t.Run("version-specific editing", func(t *testing.T) {
594614
// Test editing a specific version of a multi-version server
595615
requestBody := apiv0.ServerJSON{
616+
Schema: model.CurrentSchemaURL,
596617
Name: "com.example/multi-version-server",
597618
Description: "Updated v1.0.0 specifically",
598619
Version: "1.0.0",

internal/api/handlers/v0/publish_integration_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func TestPublishIntegration(t *testing.T) {
5757

5858
t.Run("successful publish with GitHub auth", func(t *testing.T) {
5959
publishReq := apiv0.ServerJSON{
60+
Schema: model.CurrentSchemaURL,
6061
Name: "io.github.testuser/test-mcp-server",
6162
Description: "A test MCP server for integration testing",
6263
Repository: model.Repository{
@@ -101,6 +102,7 @@ func TestPublishIntegration(t *testing.T) {
101102

102103
t.Run("successful publish with none auth (no prefix)", func(t *testing.T) {
103104
publishReq := apiv0.ServerJSON{
105+
Schema: model.CurrentSchemaURL,
104106
Name: "com.example/test-mcp-server-no-auth",
105107
Description: "A test MCP server without authentication",
106108
Repository: model.Repository{
@@ -143,7 +145,8 @@ func TestPublishIntegration(t *testing.T) {
143145

144146
t.Run("publish fails with missing authorization header", func(t *testing.T) {
145147
publishReq := apiv0.ServerJSON{
146-
Name: "test-server",
148+
Schema: model.CurrentSchemaURL,
149+
Name: "test-server",
147150
}
148151

149152
body, err := json.Marshal(publishReq)
@@ -162,6 +165,7 @@ func TestPublishIntegration(t *testing.T) {
162165

163166
t.Run("publish fails with invalid token", func(t *testing.T) {
164167
publishReq := apiv0.ServerJSON{
168+
Schema: model.CurrentSchemaURL,
165169
Name: "io.github.domdomegg/test-server",
166170
Description: "Test server",
167171
Version: "1.0.0",
@@ -183,6 +187,7 @@ func TestPublishIntegration(t *testing.T) {
183187

184188
t.Run("publish fails when permission denied", func(t *testing.T) {
185189
publishReq := apiv0.ServerJSON{
190+
Schema: model.CurrentSchemaURL,
186191
Name: "io.github.other/test-server",
187192
Description: "A test server",
188193
Version: "1.0.0",
@@ -219,6 +224,7 @@ func TestPublishIntegration(t *testing.T) {
219224

220225
t.Run("publish succeeds with MCPB package", func(t *testing.T) {
221226
publishReq := apiv0.ServerJSON{
227+
Schema: model.CurrentSchemaURL,
222228
Name: "io.github.domdomegg/airtable-mcp-server",
223229
Description: "A test server with MCPB package",
224230
Version: "1.7.2",

internal/api/handlers/v0/publish_registry_validation_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func TestPublishRegistryValidation(t *testing.T) {
4545

4646
t.Run("publish fails with npm registry validation error", func(t *testing.T) {
4747
publishReq := apiv0.ServerJSON{
48+
Schema: model.CurrentSchemaURL,
4849
Name: "com.example/test-server-with-npm",
4950
Description: "A test server with invalid npm package reference",
5051
Version: "1.0.0",
@@ -86,6 +87,7 @@ func TestPublishRegistryValidation(t *testing.T) {
8687

8788
t.Run("publish succeeds with MCPB package (registry validation enabled)", func(t *testing.T) {
8889
publishReq := apiv0.ServerJSON{
90+
Schema: model.CurrentSchemaURL,
8991
Name: "com.example/test-server-mcpb-validation",
9092
Description: "A test server with MCPB package and registry validation enabled",
9193
Version: "0.0.36",
@@ -136,6 +138,7 @@ func TestPublishRegistryValidation(t *testing.T) {
136138

137139
t.Run("publish fails when second package fails npm validation", func(t *testing.T) {
138140
publishReq := apiv0.ServerJSON{
141+
Schema: model.CurrentSchemaURL,
139142
Name: "com.example/test-server-multiple-packages",
140143
Description: "A test server with multiple packages where second fails",
141144
Version: "1.0.0",
@@ -187,6 +190,7 @@ func TestPublishRegistryValidation(t *testing.T) {
187190

188191
t.Run("publish fails when first package fails validation", func(t *testing.T) {
189192
publishReq := apiv0.ServerJSON{
193+
Schema: model.CurrentSchemaURL,
190194
Name: "com.example/test-server-first-package-fails",
191195
Description: "A test server where first package fails",
192196
Version: "1.0.0",

0 commit comments

Comments
 (0)