Implement flattened _meta format for server extensions (#331)

## Summary

This PR implements the flattened `_meta` format for server extensions as
discussed in #284, replacing the confusing wrapper format with a clean
single-file approach.

**Before (wrapper format):**
```json
{
  "server": { "name": "my-server", "description": "..." },
  "x-publisher": { "build_info": "..." },
  "x-io.modelcontextprotocol.registry": { "id": "...", "published_at": "..." }
}
```

**After (flattened format):**
```json
{
  "name": "my-server",
  "description": "...",
  "_meta": {
    "publisher": { "build_info": "..." },
    "io.modelcontextprotocol.registry": { "id": "...", "published_at": "..." }
  }
}
```

## Key Changes

- **Flattened Format**: Single `ServerJSON` file with structured `_meta`
field
- **Better DX**: Publishers maintain one file instead of separate
server.json + extensions.json
- **MCP Compliance**: Uses standard `_meta` mechanism instead of `x-*`
prefixes
- **Simplified Database**: PostgreSQL now uses simple `id, value` table
structure
- **Type Safety**: Structured `ServerMeta` with known extension fields

## Database Migration

Added migration `003_simplify_to_key_value.sql` that drops the complex
relational structure and creates a simple key-value table. This
significantly simplifies database management.

## Testing

- All core tests passing (validators, service layer)
- Schema validation working for all 10 examples
- Seed data successfully migrated (396 entries)
- Publishing functionality verified

Fixes #284
This commit is contained in:
adam jones 2025-09-01 19:27:23 +01:00 • committed by GitHub
commit 218b0b422d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -11,7 +11,7 @@ import (
"github.com/modelcontextprotocol/registry/pkg/model"
)
func ValidateServerJSON(serverJSON *model.ServerJSON) error {
func ValidateServerJSON(serverJSON *apiv0.ServerJSON) error {
// Validate server name exists and format
if _, err := parseServerName(*serverJSON); err != nil {
return err
@ -194,38 +194,45 @@ func validatePackage(pkg *model.Package) error {
}
// ValidatePublishRequest validates a complete publish request including extensions
func ValidatePublishRequest(req apiv0.PublishRequest) error {
// Validate publisher extensions
func ValidatePublishRequest(req apiv0.ServerJSON) error {
// Validate publisher extensions in _meta
if err := validatePublisherExtensions(req); err != nil {
return err
}
// Validate the server detail (includes all nested validation)
if err := ValidateServerJSON(&req.Server); err != nil {
if err := ValidateServerJSON(&req); err != nil {
return err
}
return nil
}
func validatePublisherExtensions(req apiv0.PublishRequest) error {
func validatePublisherExtensions(req apiv0.ServerJSON) error {
const maxExtensionSize = 4 * 1024 // 4KB limit
// Check size limit for x-publisher extension
if req.XPublisher != nil {
extensionsJSON, err := json.Marshal(req.XPublisher)
// Check size limit for _meta.publisher extension
if req.Meta != nil && req.Meta.Publisher != nil {
extensionsJSON, err := json.Marshal(req.Meta.Publisher)
if err != nil {
return fmt.Errorf("failed to marshal x-publisher extension: %w", err)
return fmt.Errorf("failed to marshal _meta.publisher extension: %w", err)
}
if len(extensionsJSON) > maxExtensionSize {
return fmt.Errorf("x-publisher extension exceeds 4KB limit (%d bytes)", len(extensionsJSON))
return fmt.Errorf("_meta.publisher extension exceeds 4KB limit (%d bytes)", len(extensionsJSON))
}
}
if req.Meta != nil {
// Validate that only "publisher" is allowed in _meta during publish (no registry metadata should be present)
if req.Meta.IOModelContextProtocolRegistry != nil {
return fmt.Errorf("registry metadata '_meta.io.modelcontextprotocol.registry' is not allowed during publish")
}
}
return nil
}
func parseServerName(serverJSON model.ServerJSON) (string, error) {
func parseServerName(serverJSON apiv0.ServerJSON) (string, error) {
name := serverJSON.Name
if name == "" {
return "", fmt.Errorf("server name is required and must be a string")
@ -245,7 +252,7 @@ func parseServerName(serverJSON model.ServerJSON) (string, error) {
}
// validateRemoteNamespaceMatch validates that remote URLs match the reverse-DNS namespace
func validateRemoteNamespaceMatch(serverJSON model.ServerJSON) error {
func validateRemoteNamespaceMatch(serverJSON apiv0.ServerJSON) error {
namespace := serverJSON.Name
for _, remote := range serverJSON.Remotes {