Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions internal/api/handlers/v0/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func TestPublishEndpoint(t *testing.T) {
name: "invalid token",
requestBody: apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "test-server",
Name: "example/test-server",
Description: "A test server",
Version: "1.0.0",
},
Expand Down Expand Up @@ -252,8 +252,8 @@ func TestPublishEndpoint(t *testing.T) {
},
},
setupRegistryService: func(_ service.RegistryService) {},
expectedStatus: http.StatusBadRequest,
expectedError: "server name cannot contain multiple slashes",
expectedStatus: http.StatusUnprocessableEntity,
expectedError: "expected string to match pattern",
},
{
name: "invalid server name - multiple slashes (three slashes)",
Expand All @@ -270,8 +270,8 @@ func TestPublishEndpoint(t *testing.T) {
},
},
setupRegistryService: func(_ service.RegistryService) {},
expectedStatus: http.StatusBadRequest,
expectedError: "server name cannot contain multiple slashes",
expectedStatus: http.StatusUnprocessableEntity,
expectedError: "expected string to match pattern",
},
{
name: "invalid server name - consecutive slashes",
Expand All @@ -288,8 +288,8 @@ func TestPublishEndpoint(t *testing.T) {
},
},
setupRegistryService: func(_ service.RegistryService) {},
expectedStatus: http.StatusBadRequest,
expectedError: "server name cannot contain multiple slashes",
expectedStatus: http.StatusUnprocessableEntity,
expectedError: "expected string to match pattern",
},
{
name: "invalid server name - URL-like path",
Expand All @@ -306,8 +306,8 @@ func TestPublishEndpoint(t *testing.T) {
},
},
setupRegistryService: func(_ service.RegistryService) {},
expectedStatus: http.StatusBadRequest,
expectedError: "server name cannot contain multiple slashes",
expectedStatus: http.StatusUnprocessableEntity,
expectedError: "expected string to match pattern",
},
{
name: "invalid server name - many slashes",
Expand All @@ -324,8 +324,8 @@ func TestPublishEndpoint(t *testing.T) {
},
},
setupRegistryService: func(_ service.RegistryService) {},
expectedStatus: http.StatusBadRequest,
expectedError: "server name cannot contain multiple slashes",
expectedStatus: http.StatusUnprocessableEntity,
expectedError: "expected string to match pattern",
},
{
name: "invalid server name - with packages and remotes",
Expand Down Expand Up @@ -363,8 +363,8 @@ func TestPublishEndpoint(t *testing.T) {
},
},
setupRegistryService: func(_ service.RegistryService) {},
expectedStatus: http.StatusBadRequest,
expectedError: "server name cannot contain multiple slashes",
expectedStatus: http.StatusUnprocessableEntity,
expectedError: "expected string to match pattern",
},
}

Expand Down Expand Up @@ -447,25 +447,25 @@ func TestPublishEndpoint_MultipleSlashesEdgeCases(t *testing.T) {
{
name: "invalid - trailing slash after valid name",
serverName: "com.example/server/",
expectedStatus: http.StatusBadRequest,
expectedStatus: http.StatusUnprocessableEntity,
description: "Trailing slash creates multiple slashes",
},
{
name: "invalid - leading and middle slash",
serverName: "/com.example/server",
expectedStatus: http.StatusBadRequest,
expectedStatus: http.StatusUnprocessableEntity,
description: "Leading slash with middle slash",
},
{
name: "invalid - file system style path",
serverName: "usr/local/bin/server",
expectedStatus: http.StatusBadRequest,
expectedStatus: http.StatusUnprocessableEntity,
description: "File system style paths should be rejected",
},
{
name: "invalid - version-like suffix",
serverName: "com.example/server/v1.0.0",
expectedStatus: http.StatusBadRequest,
expectedStatus: http.StatusUnprocessableEntity,
description: "Version suffixes with slash should be rejected",
},
}
Expand Down Expand Up @@ -517,9 +517,9 @@ func TestPublishEndpoint_MultipleSlashesEdgeCases(t *testing.T) {
assert.Equal(t, tc.expectedStatus, rr.Code,
"%s: expected status %d, got %d", tc.description, tc.expectedStatus, rr.Code)

if tc.expectedStatus == http.StatusBadRequest {
assert.Contains(t, rr.Body.String(), "server name cannot contain multiple slashes",
"%s: should contain specific error message", tc.description)
if tc.expectedStatus == http.StatusUnprocessableEntity {
assert.Contains(t, rr.Body.String(), "expected string to match pattern",
"%s: should contain pattern validation error", tc.description)
}
})
}
Expand Down
53 changes: 23 additions & 30 deletions pkg/api/v0/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,46 @@ import (
"github.com/modelcontextprotocol/registry/pkg/model"
)

// RegistryExtensions represents registry-generated metadata
type RegistryExtensions struct {
Status model.Status `json:"status"`
PublishedAt time.Time `json:"publishedAt"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
IsLatest bool `json:"isLatest"`
Status model.Status `json:"status" enum:"active,deprecated,deleted" doc:"Server lifecycle status"`
PublishedAt time.Time `json:"publishedAt" format:"date-time" doc:"Timestamp when the server was first published to the registry"`
UpdatedAt time.Time `json:"updatedAt,omitempty" format:"date-time" doc:"Timestamp when the server entry was last updated"`
IsLatest bool `json:"isLatest" doc:"Whether this is the latest version of the server"`
}

// ResponseMeta represents the top-level metadata in API responses
type ResponseMeta struct {
Official *RegistryExtensions `json:"io.modelcontextprotocol.registry/official,omitempty"`
Official *RegistryExtensions `json:"io.modelcontextprotocol.registry/official,omitempty" doc:"Official MCP registry metadata"`
}

// ServerResponse represents the new API response format with separated metadata
type ServerResponse struct {
Server ServerJSON `json:"server"`
Meta ResponseMeta `json:"_meta"`
Server ServerJSON `json:"server" doc:"Server configuration and metadata"`
Meta ResponseMeta `json:"_meta" doc:"Registry-managed metadata"`
}

// ServerListResponse represents the paginated server list response
type ServerListResponse struct {
Servers []ServerResponse `json:"servers"`
Metadata Metadata `json:"metadata"`
Servers []ServerResponse `json:"servers" doc:"List of server entries"`
Metadata Metadata `json:"metadata" doc:"Pagination metadata"`
}

// ServerMeta represents the structured metadata with known extension fields
type ServerMeta struct {
PublisherProvided map[string]interface{} `json:"io.modelcontextprotocol.registry/publisher-provided,omitempty"`
PublisherProvided map[string]interface{} `json:"io.modelcontextprotocol.registry/publisher-provided,omitempty" doc:"Publisher-provided metadata for downstream registries"`
}

// ServerJSON represents complete server information as defined in the MCP spec, with extension support
type ServerJSON struct {
Schema string `json:"$schema" required:"true" minLength:"1"`
Name string `json:"name" minLength:"1" maxLength:"200"`
Description string `json:"description" minLength:"1" maxLength:"100"`
Title string `json:"title,omitempty" minLength:"1" maxLength:"100"`
Repository model.Repository `json:"repository,omitempty"`
Version string `json:"version"`
WebsiteURL string `json:"websiteUrl,omitempty"`
Icons []model.Icon `json:"icons,omitempty"`
Packages []model.Package `json:"packages,omitempty"`
Remotes []model.Transport `json:"remotes,omitempty"`
Meta *ServerMeta `json:"_meta,omitempty"`
Schema string `json:"$schema" required:"true" minLength:"1" format:"uri" doc:"JSON Schema URI for this server.json format" example:"https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json"`
Name string `json:"name" minLength:"3" maxLength:"200" pattern:"^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$" doc:"Server name in reverse-DNS format. Must contain exactly one forward slash separating namespace from server name." example:"io.github.user/weather"`
Description string `json:"description" minLength:"1" maxLength:"100" doc:"Clear human-readable explanation of server functionality." example:"MCP server providing weather data and forecasts via OpenWeatherMap API"`
Title string `json:"title,omitempty" minLength:"1" maxLength:"100" doc:"Optional human-readable title or display name for the MCP server." example:"Weather API"`
Repository model.Repository `json:"repository,omitempty" doc:"Optional repository metadata for the MCP server source code."`
Version string `json:"version" doc:"Version string for this server. SHOULD follow semantic versioning." example:"1.0.2"`
WebsiteURL string `json:"websiteUrl,omitempty" format:"uri" doc:"Optional URL to the server's homepage, documentation, or project website." example:"https://modelcontextprotocol.io/examples"`
Icons []model.Icon `json:"icons,omitempty" doc:"Optional set of sized icons that the client can display in a user interface."`
Packages []model.Package `json:"packages,omitempty" doc:"Array of package configurations"`
Remotes []model.Transport `json:"remotes,omitempty" doc:"Array of remote configurations"`
Meta *ServerMeta `json:"_meta,omitempty" doc:"Extension metadata using reverse DNS namespacing for vendor-specific data"`
}

// Metadata represents pagination metadata
type Metadata struct {
NextCursor string `json:"nextCursor,omitempty"`
Count int `json:"count"`
NextCursor string `json:"nextCursor,omitempty" doc:"Pagination cursor for retrieving the next page of results. Use this exact value in the cursor query parameter of your next request."`
Count int `json:"count" doc:"Number of items in current page"`
}
Loading
Loading