## 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
200 lines
4.4 KiB
Go
200 lines
4.4 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
|
|
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
|
|
)
|
|
|
|
// MemoryDB is an in-memory implementation of the Database interface
|
|
type MemoryDB struct {
|
|
entries map[string]*apiv0.ServerJSON // maps registry metadata ID to ServerJSON
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewMemoryDB() *MemoryDB {
|
|
// Convert input ServerJSON entries to have proper metadata
|
|
serverRecords := make(map[string]*apiv0.ServerJSON)
|
|
return &MemoryDB{
|
|
entries: serverRecords,
|
|
}
|
|
}
|
|
|
|
func (db *MemoryDB) List(
|
|
ctx context.Context,
|
|
filter *ServerFilter,
|
|
cursor string,
|
|
limit int,
|
|
) ([]*apiv0.ServerJSON, string, error) {
|
|
if ctx.Err() != nil {
|
|
return nil, "", ctx.Err()
|
|
}
|
|
|
|
if limit <= 0 {
|
|
limit = 10 // Default limit
|
|
}
|
|
|
|
db.mu.RLock()
|
|
defer db.mu.RUnlock()
|
|
|
|
// Convert all entries to a slice for pagination
|
|
var allEntries []*apiv0.ServerJSON
|
|
for _, entry := range db.entries {
|
|
allEntries = append(allEntries, entry)
|
|
}
|
|
|
|
// Apply filtering and sorting
|
|
filteredEntries := db.filterAndSort(allEntries, filter)
|
|
|
|
// Find starting point for cursor-based pagination
|
|
startIdx := 0
|
|
if cursor != "" {
|
|
for i, entry := range filteredEntries {
|
|
if db.getRegistryID(entry) == cursor {
|
|
startIdx = i + 1 // Start after the cursor
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply pagination
|
|
endIdx := min(startIdx+limit, len(filteredEntries))
|
|
|
|
var result []*apiv0.ServerJSON
|
|
if startIdx < len(filteredEntries) {
|
|
result = filteredEntries[startIdx:endIdx]
|
|
} else {
|
|
result = []*apiv0.ServerJSON{}
|
|
}
|
|
|
|
// Determine next cursor
|
|
nextCursor := ""
|
|
if endIdx < len(filteredEntries) && len(result) > 0 {
|
|
nextCursor = db.getRegistryID(result[len(result)-1])
|
|
}
|
|
|
|
return result, nextCursor, nil
|
|
}
|
|
|
|
func (db *MemoryDB) GetByID(ctx context.Context, id string) (*apiv0.ServerJSON, error) {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
db.mu.RLock()
|
|
defer db.mu.RUnlock()
|
|
|
|
// Find entry by registry metadata ID
|
|
if entry, exists := db.entries[id]; exists {
|
|
// Return a copy of the ServerRecord
|
|
entryCopy := *entry
|
|
return &entryCopy, nil
|
|
}
|
|
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
func (db *MemoryDB) CreateServer(ctx context.Context, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
// Get the ID from the registry metadata
|
|
if server.Meta == nil || server.Meta.Official == nil {
|
|
return nil, fmt.Errorf("server must have registry metadata with ID")
|
|
}
|
|
|
|
id := server.Meta.Official.ID
|
|
|
|
db.mu.Lock()
|
|
defer db.mu.Unlock()
|
|
|
|
// Store the record using registry metadata ID
|
|
db.entries[id] = server
|
|
|
|
return server, nil
|
|
}
|
|
|
|
func (db *MemoryDB) UpdateServer(ctx context.Context, id string, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
db.mu.Lock()
|
|
defer db.mu.Unlock()
|
|
|
|
_, exists := db.entries[id]
|
|
if !exists {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
// Update the server
|
|
db.entries[id] = server
|
|
|
|
// Return the updated record
|
|
return server, nil
|
|
}
|
|
|
|
// For an in-memory database, this is a no-op
|
|
func (db *MemoryDB) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// filterAndSort applies filtering and sorting to the entries
|
|
func (db *MemoryDB) filterAndSort(allEntries []*apiv0.ServerJSON, filter *ServerFilter) []*apiv0.ServerJSON {
|
|
// Apply filtering
|
|
var filteredEntries []*apiv0.ServerJSON
|
|
for _, entry := range allEntries {
|
|
if db.matchesFilter(entry, filter) {
|
|
filteredEntries = append(filteredEntries, entry)
|
|
}
|
|
}
|
|
|
|
// Sort by registry metadata ID for consistent pagination
|
|
sort.Slice(filteredEntries, func(i, j int) bool {
|
|
iID := db.getRegistryID(filteredEntries[i])
|
|
jID := db.getRegistryID(filteredEntries[j])
|
|
return iID < jID
|
|
})
|
|
|
|
return filteredEntries
|
|
}
|
|
|
|
// matchesFilter checks if an entry matches the provided filter
|
|
func (db *MemoryDB) matchesFilter(entry *apiv0.ServerJSON, filter *ServerFilter) bool {
|
|
if filter == nil {
|
|
return true
|
|
}
|
|
|
|
// Check name filter
|
|
if filter.Name != nil && entry.Name != *filter.Name {
|
|
return false
|
|
}
|
|
|
|
// Check remote URL filter
|
|
if filter.RemoteURL != nil {
|
|
found := false
|
|
for _, remote := range entry.Remotes {
|
|
if remote.URL == *filter.RemoteURL {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// getRegistryID safely extracts the registry ID from an entry
|
|
func (db *MemoryDB) getRegistryID(entry *apiv0.ServerJSON) string {
|
|
if entry.Meta != nil && entry.Meta.Official != nil {
|
|
return entry.Meta.Official.ID
|
|
}
|
|
return ""
|
|
}
|