Fix _meta field format for MCP spec compliance (#367)
## Summary
Updates the `_meta` field structure to be compliant with the MCP
specification by using proper `prefix/name` format for vendor
extensions.
### Changes
- **Registry metadata**: `io.modelcontextprotocol.registry` →
`io.modelcontextprotocol.registry/official`
- **Publisher metadata**: `publisher` →
`io.modelcontextprotocol.registry/publisher-provided`
- **Database migration**: Updates indexes to use new field paths
- **Updated all code**: Service layer, validators, tests, and seed data
### Format Before
```json
{
"_meta": {
"publisher": { "license": "MIT" },
"io.modelcontextprotocol.registry": { "id": "...", "is_latest": true }
}
}
```
### Format After
```json
{
"_meta": {
"io.modelcontextprotocol.registry/publisher-provided": { "license": "MIT" },
"io.modelcontextprotocol.registry/official": { "id": "...", "is_latest": true }
}
}
```
This resolves the naming collision with the planned top-level
`publisher` field and ensures compliance with MCP specification
requirements.
Fixes #356
This commit is contained in:
parent
8f5e17d0fe
commit
ef4607720d
15 changed files with 490 additions and 466 deletions
792
data/seed.json
792
data/seed.json
File diff suppressed because it is too large
Load diff
|
|
@ -105,7 +105,7 @@ paths:
|
||||||
identifier: "@modelcontextprotocol/server-filesystem"
|
identifier: "@modelcontextprotocol/server-filesystem"
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
_meta:
|
_meta:
|
||||||
publisher:
|
io.modelcontextprotocol.registry/publisher-provided:
|
||||||
tool: "publisher-cli"
|
tool: "publisher-cli"
|
||||||
version: "1.2.3"
|
version: "1.2.3"
|
||||||
build_info:
|
build_info:
|
||||||
|
|
@ -128,7 +128,7 @@ paths:
|
||||||
identifier: "@example/mcp-demo-server"
|
identifier: "@example/mcp-demo-server"
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
_meta:
|
_meta:
|
||||||
publisher:
|
io.modelcontextprotocol.registry/publisher-provided:
|
||||||
contact_email: "maintainer@example.com"
|
contact_email: "maintainer@example.com"
|
||||||
build_metadata:
|
build_metadata:
|
||||||
commit: "abc123"
|
commit: "abc123"
|
||||||
|
|
@ -496,7 +496,7 @@ components:
|
||||||
type: object
|
type: object
|
||||||
description: Extension metadata using reverse DNS namespacing
|
description: Extension metadata using reverse DNS namespacing
|
||||||
properties:
|
properties:
|
||||||
publisher:
|
io.modelcontextprotocol.registry/publisher-provided:
|
||||||
type: object
|
type: object
|
||||||
description: Publisher-specific metadata and build information
|
description: Publisher-specific metadata and build information
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
|
|
@ -507,7 +507,7 @@ components:
|
||||||
commit: "abc123def456"
|
commit: "abc123def456"
|
||||||
timestamp: "2023-12-01T10:30:00Z"
|
timestamp: "2023-12-01T10:30:00Z"
|
||||||
pipeline_id: "build-789"
|
pipeline_id: "build-789"
|
||||||
io.modelcontextprotocol.registry:
|
io.modelcontextprotocol.registry/official:
|
||||||
type: object
|
type: object
|
||||||
description: Registry-specific metadata managed by the MCP registry system
|
description: Registry-specific metadata managed by the MCP registry system
|
||||||
required:
|
required:
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ The official registry has some more restrictions on top of this. See the [offici
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "npm-publisher",
|
"tool": "npm-publisher",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -94,7 +94,7 @@ For MCP servers located within a subdirectory of a larger repository (monorepo s
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "npm-publisher",
|
"tool": "npm-publisher",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -138,7 +138,7 @@ Suppose your MCP server application requires a `mcp start` CLI arguments to star
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "nuget-publisher",
|
"tool": "nuget-publisher",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -242,7 +242,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "ci-publisher",
|
"tool": "ci-publisher",
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -277,7 +277,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "cloud-deployer",
|
"tool": "cloud-deployer",
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -331,7 +331,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "poetry-publisher",
|
"tool": "poetry-publisher",
|
||||||
"version": "1.8.3",
|
"version": "1.8.3",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -382,7 +382,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6.
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "dotnet-publisher",
|
"tool": "dotnet-publisher",
|
||||||
"version": "8.0.100",
|
"version": "8.0.100",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -494,7 +494,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6.
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "docker-buildx",
|
"tool": "docker-buildx",
|
||||||
"version": "0.12.1",
|
"version": "0.12.1",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -578,7 +578,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6.
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "hybrid-deployer",
|
"tool": "hybrid-deployer",
|
||||||
"version": "1.7.2",
|
"version": "1.7.2",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -622,7 +622,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6.
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "mcpb-publisher",
|
"tool": "mcpb-publisher",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
@ -711,7 +711,7 @@ Some CLI tools bundle an MCP server, without a standalone MCP package or a publi
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"tool": "legacy-publisher",
|
"tool": "legacy-publisher",
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"build_info": {
|
"build_info": {
|
||||||
|
|
|
||||||
|
|
@ -453,12 +453,12 @@
|
||||||
"description": "Extension metadata using reverse DNS namespacing for vendor-specific data",
|
"description": "Extension metadata using reverse DNS namespacing for vendor-specific data",
|
||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"properties": {
|
"properties": {
|
||||||
"publisher": {
|
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Publisher-provided metadata for downstream registries",
|
"description": "Publisher-provided metadata for downstream registries",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
"io.modelcontextprotocol.registry": {
|
"io.modelcontextprotocol.registry/official": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Official MCP registry metadata (read-only, added by registry)",
|
"description": "Official MCP registry metadata (read-only, added by registry)",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ func TestEditServerEndpoint(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, published)
|
assert.NotNil(t, published)
|
||||||
assert.NotNil(t, published.Meta)
|
assert.NotNil(t, published.Meta)
|
||||||
assert.NotNil(t, published.Meta.IOModelContextProtocolRegistry)
|
assert.NotNil(t, published.Meta.Official)
|
||||||
|
|
||||||
testServerID := published.Meta.IOModelContextProtocolRegistry.ID
|
testServerID := published.Meta.Official.ID
|
||||||
|
|
||||||
// Publish a second server for permission testing
|
// Publish a second server for permission testing
|
||||||
otherServer := apiv0.ServerJSON{
|
otherServer := apiv0.ServerJSON{
|
||||||
|
|
@ -64,9 +64,9 @@ func TestEditServerEndpoint(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, otherPublished)
|
assert.NotNil(t, otherPublished)
|
||||||
assert.NotNil(t, otherPublished.Meta)
|
assert.NotNil(t, otherPublished.Meta)
|
||||||
assert.NotNil(t, otherPublished.Meta.IOModelContextProtocolRegistry)
|
assert.NotNil(t, otherPublished.Meta.Official)
|
||||||
|
|
||||||
otherServerID := otherPublished.Meta.IOModelContextProtocolRegistry.ID
|
otherServerID := otherPublished.Meta.Official.ID
|
||||||
|
|
||||||
// Publish a deleted server for undelete testing
|
// Publish a deleted server for undelete testing
|
||||||
deletedServer := apiv0.ServerJSON{
|
deletedServer := apiv0.ServerJSON{
|
||||||
|
|
@ -86,9 +86,9 @@ func TestEditServerEndpoint(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, deletedPublished)
|
assert.NotNil(t, deletedPublished)
|
||||||
assert.NotNil(t, deletedPublished.Meta)
|
assert.NotNil(t, deletedPublished.Meta)
|
||||||
assert.NotNil(t, deletedPublished.Meta.IOModelContextProtocolRegistry)
|
assert.NotNil(t, deletedPublished.Meta.Official)
|
||||||
|
|
||||||
deletedServerID := deletedPublished.Meta.IOModelContextProtocolRegistry.ID
|
deletedServerID := deletedPublished.Meta.Official.ID
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,8 @@ func TestServersListEndpoint(t *testing.T) {
|
||||||
assert.NotEmpty(t, server.Name)
|
assert.NotEmpty(t, server.Name)
|
||||||
assert.NotEmpty(t, server.Description)
|
assert.NotEmpty(t, server.Description)
|
||||||
assert.NotNil(t, server.Meta)
|
assert.NotNil(t, server.Meta)
|
||||||
assert.NotNil(t, server.Meta.IOModelContextProtocolRegistry)
|
assert.NotNil(t, server.Meta.Official)
|
||||||
assert.NotEmpty(t, server.Meta.IOModelContextProtocolRegistry.ID)
|
assert.NotEmpty(t, server.Meta.Official.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +223,7 @@ func TestServersDetailEndpoint(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "successful get server detail",
|
name: "successful get server detail",
|
||||||
serverID: testServer.Meta.IOModelContextProtocolRegistry.ID,
|
serverID: testServer.Meta.Official.ID,
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -305,7 +305,7 @@ func TestServersEndpointsIntegration(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, published)
|
assert.NotNil(t, published)
|
||||||
|
|
||||||
serverID := published.Meta.IOModelContextProtocolRegistry.ID
|
serverID := published.Meta.Official.ID
|
||||||
servers := []apiv0.ServerJSON{*published}
|
servers := []apiv0.ServerJSON{*published}
|
||||||
serverDetail := published
|
serverDetail := published
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ func TestPrometheusHandler(t *testing.T) {
|
||||||
mux.Handle("/metrics", metrics.PrometheusHandler())
|
mux.Handle("/metrics", metrics.PrometheusHandler())
|
||||||
|
|
||||||
// Create request
|
// Create request
|
||||||
url := "/v0/servers/" + server.Meta.IOModelContextProtocolRegistry.ID
|
url := "/v0/servers/" + server.Meta.Official.ID
|
||||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,11 +103,11 @@ func (db *MemoryDB) CreateServer(ctx context.Context, server *apiv0.ServerJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the ID from the registry metadata
|
// Get the ID from the registry metadata
|
||||||
if server.Meta == nil || server.Meta.IOModelContextProtocolRegistry == nil {
|
if server.Meta == nil || server.Meta.Official == nil {
|
||||||
return nil, fmt.Errorf("server must have registry metadata with ID")
|
return nil, fmt.Errorf("server must have registry metadata with ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := server.Meta.IOModelContextProtocolRegistry.ID
|
id := server.Meta.Official.ID
|
||||||
|
|
||||||
db.mu.Lock()
|
db.mu.Lock()
|
||||||
defer db.mu.Unlock()
|
defer db.mu.Unlock()
|
||||||
|
|
@ -193,8 +193,8 @@ func (db *MemoryDB) matchesFilter(entry *apiv0.ServerJSON, filter *ServerFilter)
|
||||||
|
|
||||||
// getRegistryID safely extracts the registry ID from an entry
|
// getRegistryID safely extracts the registry ID from an entry
|
||||||
func (db *MemoryDB) getRegistryID(entry *apiv0.ServerJSON) string {
|
func (db *MemoryDB) getRegistryID(entry *apiv0.ServerJSON) string {
|
||||||
if entry.Meta != nil && entry.Meta.IOModelContextProtocolRegistry != nil {
|
if entry.Meta != nil && entry.Meta.Official != nil {
|
||||||
return entry.Meta.IOModelContextProtocolRegistry.ID
|
return entry.Meta.Official.ID
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- Update database indexes to use new MCP spec compliant _meta field paths
|
||||||
|
-- Data has been updated manually, only need to update indexes
|
||||||
|
|
||||||
|
-- Drop old indexes that reference the old paths
|
||||||
|
DROP INDEX IF EXISTS idx_servers_name_latest;
|
||||||
|
DROP INDEX IF EXISTS idx_servers_updated_at;
|
||||||
|
|
||||||
|
-- Create new indexes with updated paths
|
||||||
|
CREATE INDEX idx_servers_name_latest ON servers ((value->>'name'), (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'is_latest'))
|
||||||
|
WHERE (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'is_latest')::boolean = true;
|
||||||
|
CREATE INDEX idx_servers_updated_at ON servers ((value->'_meta'->'io.modelcontextprotocol.registry/official'->>'updated_at'));
|
||||||
|
|
@ -78,7 +78,7 @@ func (db *PostgreSQL) List(
|
||||||
if _, err := uuid.Parse(cursor); err != nil {
|
if _, err := uuid.Parse(cursor); err != nil {
|
||||||
return nil, "", fmt.Errorf("invalid cursor format: %w", err)
|
return nil, "", fmt.Errorf("invalid cursor format: %w", err)
|
||||||
}
|
}
|
||||||
whereConditions = append(whereConditions, fmt.Sprintf("(value->'_meta'->'io.modelcontextprotocol.registry'->>'id') > $%d", argIndex))
|
whereConditions = append(whereConditions, fmt.Sprintf("(value->'_meta'->'io.modelcontextprotocol.registry/official'->>'id') > $%d", argIndex))
|
||||||
args = append(args, cursor)
|
args = append(args, cursor)
|
||||||
argIndex++
|
argIndex++
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +94,7 @@ func (db *PostgreSQL) List(
|
||||||
SELECT value
|
SELECT value
|
||||||
FROM servers
|
FROM servers
|
||||||
%s
|
%s
|
||||||
ORDER BY (value->'_meta'->'io.modelcontextprotocol.registry'->>'id')
|
ORDER BY (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'id')
|
||||||
LIMIT $%d
|
LIMIT $%d
|
||||||
`, whereClause, argIndex)
|
`, whereClause, argIndex)
|
||||||
args = append(args, limit)
|
args = append(args, limit)
|
||||||
|
|
@ -131,8 +131,8 @@ func (db *PostgreSQL) List(
|
||||||
nextCursor := ""
|
nextCursor := ""
|
||||||
if len(results) > 0 && len(results) >= limit {
|
if len(results) > 0 && len(results) >= limit {
|
||||||
lastResult := results[len(results)-1]
|
lastResult := results[len(results)-1]
|
||||||
if lastResult.Meta != nil && lastResult.Meta.IOModelContextProtocolRegistry != nil {
|
if lastResult.Meta != nil && lastResult.Meta.Official != nil {
|
||||||
nextCursor = lastResult.Meta.IOModelContextProtocolRegistry.ID
|
nextCursor = lastResult.Meta.Official.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ func (db *PostgreSQL) GetByID(ctx context.Context, id string) (*apiv0.ServerJSON
|
||||||
query := `
|
query := `
|
||||||
SELECT value
|
SELECT value
|
||||||
FROM servers
|
FROM servers
|
||||||
WHERE (value->'_meta'->'io.modelcontextprotocol.registry'->>'id') = $1
|
WHERE (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'id') = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
var valueJSON []byte
|
var valueJSON []byte
|
||||||
|
|
@ -177,11 +177,11 @@ func (db *PostgreSQL) CreateServer(ctx context.Context, server *apiv0.ServerJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the ID from the registry metadata
|
// Get the ID from the registry metadata
|
||||||
if server.Meta == nil || server.Meta.IOModelContextProtocolRegistry == nil {
|
if server.Meta == nil || server.Meta.Official == nil {
|
||||||
return nil, fmt.Errorf("server must have registry metadata with ID")
|
return nil, fmt.Errorf("server must have registry metadata with ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := server.Meta.IOModelContextProtocolRegistry.ID
|
id := server.Meta.Official.ID
|
||||||
|
|
||||||
// Marshal the complete server to JSONB
|
// Marshal the complete server to JSONB
|
||||||
valueJSON, err := json.Marshal(server)
|
valueJSON, err := json.Marshal(server)
|
||||||
|
|
@ -219,7 +219,7 @@ func (db *PostgreSQL) UpdateServer(ctx context.Context, id string, server *apiv0
|
||||||
query := `
|
query := `
|
||||||
UPDATE servers
|
UPDATE servers
|
||||||
SET value = $1
|
SET value = $1
|
||||||
WHERE (value->'_meta'->'io.modelcontextprotocol.registry'->>'id') = $2
|
WHERE (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'id') = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
result, err := db.conn.Exec(ctx, query, valueJSON, id)
|
result, err := db.conn.Exec(ctx, query, valueJSON, id)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func TestImportService_LocalFile(t *testing.T) {
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
},
|
},
|
||||||
Meta: &apiv0.ServerMeta{
|
Meta: &apiv0.ServerMeta{
|
||||||
IOModelContextProtocolRegistry: &apiv0.RegistryExtensions{
|
Official: &apiv0.RegistryExtensions{
|
||||||
ID: "test-id-1",
|
ID: "test-id-1",
|
||||||
PublishedAt: time.Now(),
|
PublishedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
|
|
@ -79,7 +79,7 @@ func TestImportService_HTTPFile(t *testing.T) {
|
||||||
Version: "2.0.0",
|
Version: "2.0.0",
|
||||||
},
|
},
|
||||||
Meta: &apiv0.ServerMeta{
|
Meta: &apiv0.ServerMeta{
|
||||||
IOModelContextProtocolRegistry: &apiv0.RegistryExtensions{
|
Official: &apiv0.RegistryExtensions{
|
||||||
ID: "test-id-2",
|
ID: "test-id-2",
|
||||||
PublishedAt: time.Now(),
|
PublishedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
|
|
@ -124,7 +124,7 @@ func TestImportService_RegistryAPI(t *testing.T) {
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
},
|
},
|
||||||
Meta: &apiv0.ServerMeta{
|
Meta: &apiv0.ServerMeta{
|
||||||
IOModelContextProtocolRegistry: &apiv0.RegistryExtensions{
|
Official: &apiv0.RegistryExtensions{
|
||||||
ID: "api-test-id-1",
|
ID: "api-test-id-1",
|
||||||
PublishedAt: time.Now(),
|
PublishedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
|
|
@ -144,7 +144,7 @@ func TestImportService_RegistryAPI(t *testing.T) {
|
||||||
Version: "2.0.0",
|
Version: "2.0.0",
|
||||||
},
|
},
|
||||||
Meta: &apiv0.ServerMeta{
|
Meta: &apiv0.ServerMeta{
|
||||||
IOModelContextProtocolRegistry: &apiv0.RegistryExtensions{
|
Official: &apiv0.RegistryExtensions{
|
||||||
ID: "api-test-id-2",
|
ID: "api-test-id-2",
|
||||||
PublishedAt: time.Now(),
|
PublishedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,8 @@ func (s *registryServiceImpl) Publish(req apiv0.ServerJSON) (*apiv0.ServerJSON,
|
||||||
isNewLatest := true
|
isNewLatest := true
|
||||||
if existingLatest != nil {
|
if existingLatest != nil {
|
||||||
var existingPublishedAt time.Time
|
var existingPublishedAt time.Time
|
||||||
if existingLatest.Meta != nil && existingLatest.Meta.IOModelContextProtocolRegistry != nil {
|
if existingLatest.Meta != nil && existingLatest.Meta.Official != nil {
|
||||||
existingPublishedAt = existingLatest.Meta.IOModelContextProtocolRegistry.PublishedAt
|
existingPublishedAt = existingLatest.Meta.Official.PublishedAt
|
||||||
}
|
}
|
||||||
isNewLatest = CompareVersions(
|
isNewLatest = CompareVersions(
|
||||||
serverJSON.VersionDetail.Version,
|
serverJSON.VersionDetail.Version,
|
||||||
|
|
@ -133,7 +133,7 @@ func (s *registryServiceImpl) Publish(req apiv0.ServerJSON) (*apiv0.ServerJSON,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set registry metadata
|
// Set registry metadata
|
||||||
server.Meta.IOModelContextProtocolRegistry = &apiv0.RegistryExtensions{
|
server.Meta.Official = &apiv0.RegistryExtensions{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
PublishedAt: publishTime,
|
PublishedAt: publishTime,
|
||||||
UpdatedAt: publishTime,
|
UpdatedAt: publishTime,
|
||||||
|
|
@ -150,13 +150,13 @@ func (s *registryServiceImpl) Publish(req apiv0.ServerJSON) (*apiv0.ServerJSON,
|
||||||
// Mark previous latest as no longer latest
|
// Mark previous latest as no longer latest
|
||||||
if isNewLatest && existingLatest != nil {
|
if isNewLatest && existingLatest != nil {
|
||||||
var existingLatestID string
|
var existingLatestID string
|
||||||
if existingLatest.Meta != nil && existingLatest.Meta.IOModelContextProtocolRegistry != nil {
|
if existingLatest.Meta != nil && existingLatest.Meta.Official != nil {
|
||||||
existingLatestID = existingLatest.Meta.IOModelContextProtocolRegistry.ID
|
existingLatestID = existingLatest.Meta.Official.ID
|
||||||
}
|
}
|
||||||
if existingLatestID != "" {
|
if existingLatestID != "" {
|
||||||
// Update the existing server to set is_latest = false
|
// Update the existing server to set is_latest = false
|
||||||
existingLatest.Meta.IOModelContextProtocolRegistry.IsLatest = false
|
existingLatest.Meta.Official.IsLatest = false
|
||||||
existingLatest.Meta.IOModelContextProtocolRegistry.UpdatedAt = time.Now()
|
existingLatest.Meta.Official.UpdatedAt = time.Now()
|
||||||
if _, err := s.db.UpdateServer(ctx, existingLatestID, existingLatest); err != nil {
|
if _, err := s.db.UpdateServer(ctx, existingLatestID, existingLatest); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -193,8 +193,8 @@ func (s *registryServiceImpl) validateNoDuplicateRemoteURLs(ctx context.Context,
|
||||||
// getCurrentLatestVersion finds the current latest version from existing server versions
|
// getCurrentLatestVersion finds the current latest version from existing server versions
|
||||||
func (s *registryServiceImpl) getCurrentLatestVersion(existingServerVersions []*apiv0.ServerJSON) *apiv0.ServerJSON {
|
func (s *registryServiceImpl) getCurrentLatestVersion(existingServerVersions []*apiv0.ServerJSON) *apiv0.ServerJSON {
|
||||||
for _, server := range existingServerVersions {
|
for _, server := range existingServerVersions {
|
||||||
if server.Meta != nil && server.Meta.IOModelContextProtocolRegistry != nil &&
|
if server.Meta != nil && server.Meta.Official != nil &&
|
||||||
server.Meta.IOModelContextProtocolRegistry.IsLatest {
|
server.Meta.Official.IsLatest {
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -251,21 +251,21 @@ func ValidatePublishRequest(req apiv0.ServerJSON, cfg *config.Config) error {
|
||||||
func validatePublisherExtensions(req apiv0.ServerJSON) error {
|
func validatePublisherExtensions(req apiv0.ServerJSON) error {
|
||||||
const maxExtensionSize = 4 * 1024 // 4KB limit
|
const maxExtensionSize = 4 * 1024 // 4KB limit
|
||||||
|
|
||||||
// Check size limit for _meta.publisher extension
|
// Check size limit for _meta publisher-provided extension
|
||||||
if req.Meta != nil && req.Meta.Publisher != nil {
|
if req.Meta != nil && req.Meta.PublisherProvided != nil {
|
||||||
extensionsJSON, err := json.Marshal(req.Meta.Publisher)
|
extensionsJSON, err := json.Marshal(req.Meta.PublisherProvided)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal _meta.publisher extension: %w", err)
|
return fmt.Errorf("failed to marshal _meta.io.modelcontextprotocol.registry/publisher-provided extension: %w", err)
|
||||||
}
|
}
|
||||||
if len(extensionsJSON) > maxExtensionSize {
|
if len(extensionsJSON) > maxExtensionSize {
|
||||||
return fmt.Errorf("_meta.publisher extension exceeds 4KB limit (%d bytes)", len(extensionsJSON))
|
return fmt.Errorf("_meta.io.modelcontextprotocol.registry/publisher-provided extension exceeds 4KB limit (%d bytes)", len(extensionsJSON))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Meta != nil {
|
if req.Meta != nil {
|
||||||
// Validate that only "publisher" is allowed in _meta during publish (no registry metadata should be present)
|
// Validate that only publisher-provided data is allowed in _meta during publish (no official registry metadata should be present)
|
||||||
if req.Meta.IOModelContextProtocolRegistry != nil {
|
if req.Meta.Official != nil {
|
||||||
return fmt.Errorf("registry metadata '_meta.io.modelcontextprotocol.registry' is not allowed during publish")
|
return fmt.Errorf("official registry metadata '_meta.io.modelcontextprotocol.registry/official' is not allowed during publish")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ type ServerListResponse struct {
|
||||||
|
|
||||||
// ServerMeta represents the structured metadata with known extension fields
|
// ServerMeta represents the structured metadata with known extension fields
|
||||||
type ServerMeta struct {
|
type ServerMeta struct {
|
||||||
Publisher map[string]interface{} `json:"publisher,omitempty"`
|
Official *RegistryExtensions `json:"io.modelcontextprotocol.registry/official,omitempty"`
|
||||||
IOModelContextProtocolRegistry *RegistryExtensions `json:"io.modelcontextprotocol.registry,omitempty"`
|
PublisherProvided map[string]interface{} `json:"io.modelcontextprotocol.registry/publisher-provided,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerJSON represents complete server information as defined in the MCP spec, with extension support
|
// ServerJSON represents complete server information as defined in the MCP spec, with extension support
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,10 @@ func publishToRegistry(expected *apiv0.ServerJSON, line int) error {
|
||||||
|
|
||||||
id, err := runPublisher(p)
|
id, err := runPublisher(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf(" ⛔ Failed to get server ID: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Printf(" 📋 Got server ID: %s", id)
|
||||||
|
|
||||||
return verifyPublishedServer(id, expected)
|
return verifyPublishedServer(id, expected)
|
||||||
}
|
}
|
||||||
|
|
@ -159,6 +161,9 @@ func runPublisher(filePath string) (string, error) {
|
||||||
return "", fmt.Errorf("failed to get server name from file: %w", err)
|
return "", fmt.Errorf("failed to get server name from file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a small delay to ensure database consistency
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
// Find the server in the registry by name
|
// Find the server in the registry by name
|
||||||
return findServerIDByName(serverName)
|
return findServerIDByName(serverName)
|
||||||
}
|
}
|
||||||
|
|
@ -216,16 +221,24 @@ func findServerIDByName(serverName string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the server with matching name
|
// Find the server with matching name
|
||||||
|
var foundServers []string
|
||||||
for _, server := range serverList.Servers {
|
for _, server := range serverList.Servers {
|
||||||
if server.Name == serverName && server.Meta.IOModelContextProtocolRegistry.IsLatest {
|
if server.Name == serverName {
|
||||||
return server.Meta.IOModelContextProtocolRegistry.ID, nil
|
foundServers = append(foundServers, fmt.Sprintf("ID:%s IsLatest:%t", server.Meta.Official.ID, server.Meta.Official.IsLatest))
|
||||||
|
if server.Meta.Official.IsLatest {
|
||||||
|
return server.Meta.Official.ID, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("could not find server with name %s", serverName)
|
if len(foundServers) > 0 {
|
||||||
|
return "", fmt.Errorf("found server %s but none marked as latest: %v", serverName, foundServers)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("could not find any server with name %s", serverName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyPublishedServer(id string, expected *apiv0.ServerJSON) error {
|
func verifyPublishedServer(id string, expected *apiv0.ServerJSON) error {
|
||||||
|
log.Printf(" 🔍 Verifying server with ID: %s", id)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, registryURL+"/v0/servers/"+id, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, registryURL+"/v0/servers/"+id, nil)
|
||||||
|
|
@ -324,12 +337,12 @@ func compareServerJSON(expected, actual *apiv0.ServerJSON) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare only publisher metadata if present
|
// Compare only publisher metadata if present
|
||||||
if expected.Meta != nil && expected.Meta.Publisher != nil {
|
if expected.Meta != nil && expected.Meta.PublisherProvided != nil {
|
||||||
if actual.Meta == nil || actual.Meta.Publisher == nil {
|
if actual.Meta == nil || actual.Meta.PublisherProvided == nil {
|
||||||
return fmt.Errorf("expected publisher metadata, but got none")
|
return fmt.Errorf("expected publisher metadata, but got none")
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(expected.Meta.Publisher, actual.Meta.Publisher) {
|
if !reflect.DeepEqual(expected.Meta.PublisherProvided, actual.Meta.PublisherProvided) {
|
||||||
return fmt.Errorf("publisher metadata mismatch: expected %+v, got %+v", expected.Meta.Publisher, actual.Meta.Publisher)
|
return fmt.Errorf("publisher metadata mismatch: expected %+v, got %+v", expected.Meta.PublisherProvided, actual.Meta.PublisherProvided)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue