Deduplicate server JSON structs between publisher and registry (#318)

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

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->

The following PR eliminates duplicate struct definitions by migrating
the publisher tool to use canonical types from pkg/model, creating a
single source of truth across the codebase. The publisher tool
previously had its own copies of structs that were duplicated, ie.
ServerJSON, Package, Repository, VersionDetail, EnvironmentVariable,
RuntimeArgument and a few others which was not optimal


**Changes in the PR:**
- Refactored the structs to single canonical types located at pkg/model
- Refactor the implementation to use/build canonical structures directly
  - Remove all duplicate definitions (5 structs eliminated)
- Publisher now uses model.ServerJSON, model.Package, model.Repository,
etc.
- Renamed some of the structs to better reflect their use case, i.e.
ServerJSON instead of ServerDetail
- Fixes an issue where the Remote property was missing from the previous
publisher copy of the Server

**Benefits:**
  - Single source of truth - Schema changes in one place
  - Type safety - Compiler catches mismatches
  - Enhanced validation - Field constraints and bson tags
  - Zero breaking changes - Same CLI interface and JSON output
  - Future-proof - New canonical fields automatically available

Supersedes: #217 and #240

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

Locally

## Breaking Changes
<!-- Will users need to update their code or configurations? -->
No

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [x] 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-08-28 16:58:15 +03:00 • committed by GitHub
commit 099a2d702e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -17,7 +17,8 @@ import (
"github.com/modelcontextprotocol/registry/internal/auth"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/model"
apiv1 "github.com/modelcontextprotocol/registry/pkg/api/v1"
"github.com/modelcontextprotocol/registry/pkg/model"
)
func TestEditServerEndpoint(t *testing.T) {
@ -36,7 +37,7 @@ func TestEditServerEndpoint(t *testing.T) {
authHeader: func() string {
cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"}
token, _ := generateTestJWTToken(cfg, auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethod: auth.MethodGitHubAT,
AuthMethodSubject: "domdomegg",
Permissions: []auth.Permission{
{Action: auth.PermissionActionEdit, ResourcePattern: "io.github.domdomegg/*"},
@ -44,11 +45,11 @@ func TestEditServerEndpoint(t *testing.T) {
})
return "Bearer " + token
}(),
requestBody: model.PublishRequest{
Server: model.ServerDetail{
requestBody: apiv1.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Updated test server",
Status: model.ServerStatusDeprecated,
Status: model.StatusDeprecated,
Repository: model.Repository{
URL: "https://github.com/domdomegg/test-server",
Source: "github",
@ -61,23 +62,23 @@ func TestEditServerEndpoint(t *testing.T) {
},
setupMocks: func(registry *MockRegistryService) {
// Current server (not deleted)
currentServer := &model.ServerResponse{
Server: model.ServerDetail{
currentServer := &apiv1.ServerRecord{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Original server",
Status: model.ServerStatusActive,
Status: model.StatusActive,
},
}
registry.On("GetByID", "550e8400-e29b-41d4-a716-446655440001").Return(currentServer, nil)
expectedResponse := &model.ServerResponse{
Server: model.ServerDetail{
expectedResponse := &apiv1.ServerRecord{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Updated test server",
Status: model.ServerStatusDeprecated,
Status: model.StatusDeprecated,
},
}
registry.On("EditServer", "550e8400-e29b-41d4-a716-446655440001", mock.AnythingOfType("model.PublishRequest")).Return(expectedResponse, nil)
registry.On("EditServer", "550e8400-e29b-41d4-a716-446655440001", mock.AnythingOfType("v1.PublishRequest")).Return(expectedResponse, nil)
},
expectedStatus: http.StatusOK,
},
@ -85,7 +86,7 @@ func TestEditServerEndpoint(t *testing.T) {
name: "missing authorization header",
serverID: "550e8400-e29b-41d4-a716-446655440001",
authHeader: "",
requestBody: model.PublishRequest{},
requestBody: apiv1.PublishRequest{},
setupMocks: func(_ *MockRegistryService) {},
expectedStatus: 422,
expectedError: "required header parameter is missing",
@ -94,7 +95,7 @@ func TestEditServerEndpoint(t *testing.T) {
name: "invalid authorization header format",
serverID: "550e8400-e29b-41d4-a716-446655440001",
authHeader: "InvalidFormat token123",
requestBody: model.PublishRequest{},
requestBody: apiv1.PublishRequest{},
setupMocks: func(_ *MockRegistryService) {},
expectedStatus: http.StatusUnauthorized,
expectedError: "Unauthorized",
@ -103,7 +104,7 @@ func TestEditServerEndpoint(t *testing.T) {
name: "invalid token",
serverID: "550e8400-e29b-41d4-a716-446655440001",
authHeader: "Bearer invalid-token",
requestBody: model.PublishRequest{},
requestBody: apiv1.PublishRequest{},
setupMocks: func(_ *MockRegistryService) {},
expectedStatus: http.StatusUnauthorized,
expectedError: "Unauthorized",
@ -114,7 +115,7 @@ func TestEditServerEndpoint(t *testing.T) {
authHeader: func() string {
cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"}
token, _ := generateTestJWTToken(cfg, auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethod: auth.MethodGitHubAT,
AuthMethodSubject: "domdomegg",
Permissions: []auth.Permission{
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.domdomegg/*"},
@ -122,19 +123,19 @@ func TestEditServerEndpoint(t *testing.T) {
})
return "Bearer " + token
}(),
requestBody: model.PublishRequest{
Server: model.ServerDetail{
requestBody: apiv1.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Updated test server",
},
},
setupMocks: func(registry *MockRegistryService) {
// Need to mock GetByID since we check permissions against existing server name
currentServer := &model.ServerResponse{
Server: model.ServerDetail{
currentServer := &apiv1.ServerRecord{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Original server",
Status: model.ServerStatusActive,
Status: model.StatusActive,
},
}
registry.On("GetByID", "550e8400-e29b-41d4-a716-446655440001").Return(currentServer, nil)
@ -148,7 +149,7 @@ func TestEditServerEndpoint(t *testing.T) {
authHeader: func() string {
cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"}
token, _ := generateTestJWTToken(cfg, auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethod: auth.MethodGitHubAT,
AuthMethodSubject: "domdomegg",
Permissions: []auth.Permission{
{Action: auth.PermissionActionEdit, ResourcePattern: "io.github.domdomegg/*"},
@ -156,8 +157,8 @@ func TestEditServerEndpoint(t *testing.T) {
})
return "Bearer " + token
}(),
requestBody: model.PublishRequest{
Server: model.ServerDetail{
requestBody: apiv1.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.other/test-server",
Description: "Updated test server",
},
@ -165,11 +166,11 @@ func TestEditServerEndpoint(t *testing.T) {
setupMocks: func(registry *MockRegistryService) {
// Need to mock GetByID since we check permissions against existing server name
// This test case shows a different scenario: existing server is "other" but user only has perms for "domdomegg"
currentServer := &model.ServerResponse{
Server: model.ServerDetail{
currentServer := &apiv1.ServerRecord{
Server: model.ServerJSON{
Name: "io.github.other/test-server",
Description: "Original server",
Status: model.ServerStatusActive,
Status: model.StatusActive,
},
}
registry.On("GetByID", "550e8400-e29b-41d4-a716-446655440001").Return(currentServer, nil)
@ -183,7 +184,7 @@ func TestEditServerEndpoint(t *testing.T) {
authHeader: func() string {
cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"}
token, _ := generateTestJWTToken(cfg, auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethod: auth.MethodGitHubAT,
AuthMethodSubject: "domdomegg",
Permissions: []auth.Permission{
{Action: auth.PermissionActionEdit, ResourcePattern: "io.github.domdomegg/*"},
@ -191,8 +192,8 @@ func TestEditServerEndpoint(t *testing.T) {
})
return "Bearer " + token
}(),
requestBody: model.PublishRequest{
Server: model.ServerDetail{
requestBody: apiv1.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Updated test server",
},
@ -209,7 +210,7 @@ func TestEditServerEndpoint(t *testing.T) {
authHeader: func() string {
cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"}
token, _ := generateTestJWTToken(cfg, auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethod: auth.MethodGitHubAT,
AuthMethodSubject: "domdomegg",
Permissions: []auth.Permission{
{Action: auth.PermissionActionEdit, ResourcePattern: "*"},
@ -217,23 +218,23 @@ func TestEditServerEndpoint(t *testing.T) {
})
return "Bearer " + token
}(),
requestBody: model.PublishRequest{
Server: model.ServerDetail{
requestBody: apiv1.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Updated test server",
},
},
setupMocks: func(registry *MockRegistryService) {
// Current server (not deleted)
currentServer := &model.ServerResponse{
Server: model.ServerDetail{
currentServer := &apiv1.ServerRecord{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Original server",
Status: model.ServerStatusActive,
Status: model.StatusActive,
},
}
registry.On("GetByID", "550e8400-e29b-41d4-a716-446655440001").Return(currentServer, nil)
registry.On("EditServer", "550e8400-e29b-41d4-a716-446655440001", mock.AnythingOfType("model.PublishRequest")).Return(nil, errors.New("database error"))
registry.On("EditServer", "550e8400-e29b-41d4-a716-446655440001", mock.AnythingOfType("v1.PublishRequest")).Return(nil, errors.New("database error"))
},
expectedStatus: http.StatusBadRequest,
expectedError: "Bad Request",
@ -244,7 +245,7 @@ func TestEditServerEndpoint(t *testing.T) {
authHeader: func() string {
cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"}
token, _ := generateTestJWTToken(cfg, auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethod: auth.MethodGitHubAT,
AuthMethodSubject: "domdomegg",
Permissions: []auth.Permission{
{Action: auth.PermissionActionEdit, ResourcePattern: "*"},
@ -263,7 +264,7 @@ func TestEditServerEndpoint(t *testing.T) {
authHeader: func() string {
cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"}
token, _ := generateTestJWTToken(cfg, auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethod: auth.MethodGitHubAT,
AuthMethodSubject: "domdomegg",
Permissions: []auth.Permission{
{Action: auth.PermissionActionEdit, ResourcePattern: "*"},
@ -271,20 +272,20 @@ func TestEditServerEndpoint(t *testing.T) {
})
return "Bearer " + token
}(),
requestBody: model.PublishRequest{
Server: model.ServerDetail{
requestBody: apiv1.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Trying to undelete server",
Status: model.ServerStatusActive,
Status: model.StatusActive,
},
},
setupMocks: func(registry *MockRegistryService) {
// Current server is deleted
currentServer := &model.ServerResponse{
Server: model.ServerDetail{
currentServer := &apiv1.ServerRecord{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Original server",
Status: model.ServerStatusDeleted,
Status: model.StatusDeleted,
},
}
registry.On("GetByID", "550e8400-e29b-41d4-a716-446655440001").Return(currentServer, nil)
@ -345,4 +346,4 @@ func TestEditServerEndpoint(t *testing.T) {
mockRegistry.AssertExpectations(t)
})
}
}
}