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
3 changed files with 19375 additions and 19670 deletions

View file

@ -11,10 +11,10 @@ import (
"strings"
"time"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
"github.com/modelcontextprotocol/registry/pkg/model"
)
func InitCommand() error {
// Check if server.json already exists
if _, err := os.Stat("server.json"); err == nil {
@ -266,7 +266,7 @@ func createServerJSON(
name, description, version, repoURL, repoSource,
packageType, packageIdentifier, packageVersion string,
envVars []model.KeyValueInput,
) model.ServerJSON {
) apiv0.ServerJSON {
// Determine registry type and base URL
var registryType, registryBaseURL string
switch packageType {
@ -297,7 +297,7 @@ func createServerJSON(
}
// Create server structure
return model.ServerJSON{
return apiv0.ServerJSON{
Schema: "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
Name: name,
Description: description,

View file

@ -11,6 +11,8 @@ import (
"os"
"path/filepath"
"strings"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
)
func PublishCommand(args []string) error {
@ -30,7 +32,7 @@ func PublishCommand(args []string) error {
}
// Validate JSON
var serverJSON map[string]any
var serverJSON apiv0.ServerJSON
if err := json.Unmarshal(serverData, &serverJSON); err != nil {
return fmt.Errorf("invalid server.json: %w", err)
}
@ -73,26 +75,14 @@ func PublishCommand(args []string) error {
func publishToRegistry(registryURL string, serverData []byte, token string) error {
// Parse the server JSON data
var serverDetails map[string]any
err := json.Unmarshal(serverData, &serverDetails)
var serverJSON apiv0.ServerJSON
err := json.Unmarshal(serverData, &serverJSON)
if err != nil {
return fmt.Errorf("error parsing server.json file: %w", err)
}
// Create the publish request payload
var publishReq map[string]any
if _, hasServerField := serverDetails["server"]; hasServerField {
// Already in PublishRequest format
publishReq = serverDetails
} else {
// Wrap in server field
publishReq = map[string]any{
"server": serverDetails,
}
}
// Convert to JSON
jsonData, err := json.Marshal(publishReq)
jsonData, err := json.Marshal(serverJSON)
if err != nil {
return fmt.Errorf("error serializing request: %w", err)
}
@ -129,4 +119,4 @@ func publishToRegistry(registryURL string, serverData []byte, token string) erro
}
return nil
}
}

View file

@ -16,7 +16,7 @@ import (
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/service"
"github.com/modelcontextprotocol/registry/internal/telemetry"
"github.com/modelcontextprotocol/registry/pkg/model"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
)
func main() {
@ -46,7 +46,7 @@ func main() {
// Initialize services based on environment
switch cfg.DatabaseType {
case config.DatabaseTypeMemory:
db = database.NewMemoryDB(map[string]*model.ServerJSON{})
db = database.NewMemoryDB(map[string]*apiv0.ServerJSON{})
registryService = service.NewRegistryServiceWithDB(db)
case config.DatabaseTypePostgreSQL:
// Use PostgreSQL for real registry service