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:
adam jones 2025-09-08 13:55:33 -07:00 • committed by GitHub
commit a85456a9b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 262 additions and 13 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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