Add filtering functionality to server list endpoint (#374)
## Summary - Adds `updated_since`, `search`, and `version` query parameters to `GET /v0/servers` - Enables incremental synchronization via RFC3339 timestamp filtering - Provides simple substring search on server names - Supports latest version filtering with `version=latest` This implementation provides the core filtering capabilities needed for efficient registry synchronization and improved server discovery. Fixes #291 Fixes #135
This commit is contained in:
parent
be6c2d9142
commit
a85456a9b9
3 changed files with 262 additions and 13 deletions
|
|
@ -3,6 +3,7 @@ package database
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
|
||||
)
|
||||
|
|
@ -19,8 +20,12 @@ var (
|
|||
|
||||
// ServerFilter defines filtering options for server queries
|
||||
type ServerFilter struct {
|
||||
Name *string // for finding versions of same server
|
||||
RemoteURL *string // for duplicate URL detection
|
||||
Name *string // for finding versions of same server
|
||||
RemoteURL *string // for duplicate URL detection
|
||||
UpdatedSince *time.Time // for incremental sync filtering
|
||||
SubstringName *string // for substring search on name
|
||||
Version *string // for exact version matching
|
||||
IsLatest *bool // for filtering latest versions only
|
||||
}
|
||||
|
||||
// Database defines the interface for database operations
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
|
||||
|
|
@ -164,6 +165,7 @@ func (db *MemoryDB) filterAndSort(allEntries []*apiv0.ServerJSON, filter *Server
|
|||
}
|
||||
|
||||
// matchesFilter checks if an entry matches the provided filter
|
||||
//nolint:cyclop // Filter matching logic is inherently complex but clear
|
||||
func (db *MemoryDB) matchesFilter(entry *apiv0.ServerJSON, filter *ServerFilter) bool {
|
||||
if filter == nil {
|
||||
return true
|
||||
|
|
@ -188,6 +190,44 @@ func (db *MemoryDB) matchesFilter(entry *apiv0.ServerJSON, filter *ServerFilter)
|
|||
}
|
||||
}
|
||||
|
||||
// Check updatedSince filter
|
||||
if filter.UpdatedSince != nil {
|
||||
if entry.Meta == nil || entry.Meta.Official == nil {
|
||||
return false
|
||||
}
|
||||
if entry.Meta.Official.UpdatedAt.Before(*filter.UpdatedSince) ||
|
||||
entry.Meta.Official.UpdatedAt.Equal(*filter.UpdatedSince) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check name search filter (substring match)
|
||||
if filter.SubstringName != nil {
|
||||
// Case-insensitive substring search
|
||||
searchLower := strings.ToLower(*filter.SubstringName)
|
||||
nameLower := strings.ToLower(entry.Name)
|
||||
if !strings.Contains(nameLower, searchLower) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check exact version filter
|
||||
if filter.Version != nil {
|
||||
if entry.VersionDetail.Version != *filter.Version {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check is_latest filter
|
||||
if filter.IsLatest != nil {
|
||||
if entry.Meta == nil || entry.Meta.Official == nil {
|
||||
return false
|
||||
}
|
||||
if entry.Meta.Official.IsLatest != *filter.IsLatest {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ func NewPostgreSQL(ctx context.Context, connectionURI string) (*PostgreSQL, erro
|
|||
}, nil
|
||||
}
|
||||
|
||||
//nolint:cyclop // Database filtering logic is inherently complex but clear
|
||||
func (db *PostgreSQL) List(
|
||||
ctx context.Context,
|
||||
filter *ServerFilter,
|
||||
|
|
@ -71,6 +72,26 @@ func (db *PostgreSQL) List(
|
|||
args = append(args, *filter.RemoteURL)
|
||||
argIndex++
|
||||
}
|
||||
if filter.UpdatedSince != nil {
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("(value->'_meta'->'io.modelcontextprotocol.registry/official'->>'updated_at')::timestamp > $%d", argIndex))
|
||||
args = append(args, *filter.UpdatedSince)
|
||||
argIndex++
|
||||
}
|
||||
if filter.SubstringName != nil {
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("value->>'name' ILIKE $%d", argIndex))
|
||||
args = append(args, "%"+*filter.SubstringName+"%")
|
||||
argIndex++
|
||||
}
|
||||
if filter.Version != nil {
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("(value->'version_detail'->>'version') = $%d", argIndex))
|
||||
args = append(args, *filter.Version)
|
||||
argIndex++
|
||||
}
|
||||
if filter.IsLatest != nil {
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("(value->'_meta'->'io.modelcontextprotocol.registry/official'->>'is_latest')::boolean = $%d", argIndex))
|
||||
args = append(args, *filter.IsLatest)
|
||||
argIndex++
|
||||
}
|
||||
}
|
||||
|
||||
// Add cursor pagination using registry metadata ID
|
||||
|
|
|
|||
Loading…
Reference in a new issue