Skip to content

feat: continuous verification job #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 29 commits into
base: remote-verification
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
72a1967
feat: implement 128-bit cryptographically secure token generation for…
aphansal123 Jul 29, 2025
ffbed48
Merge branch 'main' into feature/domain-verification-token-generation
aphansal123 Jul 29, 2025
f375640
Fix package references and eliminate duplicate string literals in tok…
aphansal123 Jul 29, 2025
f7a85fa
Update internal/verification/README.md
aphansal123 Jul 29, 2025
36579a1
Update internal/verification/token_test.go
aphansal123 Jul 29, 2025
382925e
Capitalize MaxDNSRecordLength variable name for consistency
aphansal123 Jul 29, 2025
a9afe32
Add comprehensive DNS TXT record RFC compliance validation tests
aphansal123 Jul 29, 2025
bc4d621
Fix gci linter error by removing trailing whitespace
aphansal123 Jul 29, 2025
71b6a9f
Initial implementation of the metadata collection
thomas-sickert Jul 31, 2025
c3cd6f7
Merge branch 'remote-verification' of https://github.com/modelcontext…
thomas-sickert Jul 31, 2025
32af3a1
feat: dns record verification (#208)
aphansal123 Aug 3, 2025
3daef44
Update to account for the verification without server case
thomas-sickert Aug 4, 2025
5daa853
Implement continuous verification background job system with DNS/HTTP…
aphansal123 Aug 4, 2025
b48197b
Fix database interface methods and refine background verification job…
aphansal123 Aug 4, 2025
57fe1ad
Merge branch 'remote-verification' into thomas-sickert/token-storage
aphansal123 Aug 4, 2025
8b04448
Merge branch 'thomas-sickert/token-storage' into feature/continuous-v…
aphansal123 Aug 4, 2025
92b8406
Fix linting errors and modernize verification code with TLS 1.2+, pro…
aphansal123 Aug 4, 2025
40865cd
Fix build errors and duplicate struct definitions, add missing databa…
aphansal123 Aug 4, 2025
60999f9
fix: update cron schedule comment and remove unused variable assignment
aphansal123 Aug 4, 2025
b981b95
feat: integrate background verification job into main application
aphansal123 Aug 5, 2025
1981eb8
fix: resolve golangci-lint import formatting issues
aphansal123 Aug 5, 2025
a348eef
Add production-ready background verification job with configurable en…
aphansal123 Aug 5, 2025
39efcf9
Merge branch 'remote-verification' into feature/continuous-verificati…
aphansal123 Aug 5, 2025
426428b
update temporary network error to clarify that it is non-retryable
aphansal123 Aug 5, 2025
680e739
Fix negative timeout bug in background verification job
aphansal123 Aug 5, 2025
b479a75
Upgrade env package from v10 to v11 and fix golangci-lint errors
aphansal123 Aug 5, 2025
75312ec
Merge branch 'remote-verification' into feature/continuous-verificati…
aphansal123 Aug 5, 2025
af1627e
Fix notification logic to alert on all threshold exceedances and upda…
aphansal123 Aug 5, 2025
6d0d5d6
Fix background job to use stored tokens instead of generating new ones
aphansal123 Aug 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,29 @@ The service can be configured using environment variables:
| `MCP_REGISTRY_SEED_IMPORT` | Import `seed.json` on first run | `true` |
| `MCP_REGISTRY_SERVER_ADDRESS` | Listen address for the server | `:8080` |

### Background Job Configuration

The registry includes a background verification job that continuously validates domain ownership for registered servers:

| Variable | Description | Default |
|----------|-------------|---------|
| `MCP_REGISTRY_BACKGROUND_JOB_ENABLED` | Enable background verification job | `true` |
| `MCP_REGISTRY_BACKGROUND_JOB_CRON_SCHEDULE` | Cron schedule for verification runs | `0 0 2 * * *` (daily at 2 AM) |
| `MCP_REGISTRY_BACKGROUND_JOB_MAX_CONCURRENT` | Maximum concurrent verifications | `10` |
| `MCP_REGISTRY_BACKGROUND_JOB_VERIFICATION_TIMEOUT_SECONDS` | Timeout for each verification (seconds) | `30` |
| `MCP_REGISTRY_BACKGROUND_JOB_FAILURE_THRESHOLD` | Consecutive failures before marking as failed | `3` |
| `MCP_REGISTRY_BACKGROUND_JOB_NOTIFICATION_COOLDOWN_HOURS` | Hours between failure notifications | `24` |
| `MCP_REGISTRY_BACKGROUND_JOB_CLEANUP_INTERVAL_DAYS` | Days between cleanup of old records | `7` |

Example production configuration:
```bash
# Enable background job with custom schedule (every 6 hours)
MCP_REGISTRY_BACKGROUND_JOB_ENABLED=true
MCP_REGISTRY_BACKGROUND_JOB_CRON_SCHEDULE="0 0 */6 * * *"
MCP_REGISTRY_BACKGROUND_JOB_MAX_CONCURRENT=20
MCP_REGISTRY_BACKGROUND_JOB_VERIFICATION_TIMEOUT_SECONDS=60
```


## Testing

Expand Down
37 changes: 37 additions & 0 deletions cmd/registry/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/model"
"github.com/modelcontextprotocol/registry/internal/service"
"github.com/modelcontextprotocol/registry/internal/verification"
)

func main() {
Expand Down Expand Up @@ -93,6 +94,42 @@ func main() {
}
}

// Initialize background verification job (if enabled)
var backgroundJob *verification.BackgroundVerificationJob
if cfg.BackgroundJobEnabled {
// Convert config to verification background job config
backgroundJobConfig := &verification.BackgroundJobConfig{
CronSchedule: cfg.BackgroundJobCronSchedule,
MaxConcurrentVerifications: cfg.BackgroundJobMaxConcurrentVerifications,
VerificationTimeout: time.Duration(cfg.BackgroundJobVerificationTimeoutSeconds) * time.Second,
FailureThreshold: cfg.BackgroundJobFailureThreshold,
RetryDelay: time.Second,
NotificationCooldown: time.Duration(cfg.BackgroundJobNotificationCooldownHours) * time.Hour,
CleanupInterval: time.Duration(cfg.BackgroundJobCleanupIntervalDays) * 24 * time.Hour,
}

backgroundJob = verification.NewBackgroundVerificationJob(db, backgroundJobConfig, nil)

// Start background verification job
ctx := context.Background()
if err := backgroundJob.Start(ctx); err != nil {
log.Printf("Failed to start background verification job: %v", err)
} else {
log.Println("Background verification job started successfully")
}

// Defer stopping the background job
defer func() {
if err := backgroundJob.Stop(); err != nil {
log.Printf("Error stopping background verification job: %v", err)
} else {
log.Println("Background verification job stopped successfully")
}
}()
} else {
log.Println("Background verification job is disabled")
}

// Initialize authentication services
authService := auth.NewAuthService(cfg)

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.23.0
require (
github.com/caarlos0/env/v11 v11.3.1
github.com/google/uuid v1.6.0
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/stretchr/testify v1.10.0
github.com/swaggo/files v1.0.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
Expand Down
11 changes: 10 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package config

import (
env "github.com/caarlos0/env/v11"
"github.com/caarlos0/env/v11"
Copy link
Preview

Copilot AI Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import alias has been removed (changed from env \"github.com/caarlos0/env/v11\" to just the standard import). This is good practice as the package name env is already clear and the alias was unnecessary.

Copilot uses AI. Check for mistakes.

)

type DatabaseType string
Expand All @@ -25,6 +25,15 @@ type Config struct {
Version string `env:"VERSION" envDefault:"dev"`
GithubClientID string `env:"GITHUB_CLIENT_ID" envDefault:""`
GithubClientSecret string `env:"GITHUB_CLIENT_SECRET" envDefault:""`

// Background verification job configuration
BackgroundJobEnabled bool `env:"BACKGROUND_JOB_ENABLED" envDefault:"true"`
BackgroundJobCronSchedule string `env:"BACKGROUND_JOB_CRON_SCHEDULE" envDefault:"0 0 2 * * *"`
BackgroundJobMaxConcurrentVerifications int `env:"BACKGROUND_JOB_MAX_CONCURRENT" envDefault:"10"`
BackgroundJobVerificationTimeoutSeconds int `env:"BACKGROUND_JOB_VERIFICATION_TIMEOUT_SECONDS" envDefault:"30"`
BackgroundJobFailureThreshold int `env:"BACKGROUND_JOB_FAILURE_THRESHOLD" envDefault:"3"`
BackgroundJobNotificationCooldownHours int `env:"BACKGROUND_JOB_NOTIFICATION_COOLDOWN_HOURS" envDefault:"24"`
BackgroundJobCleanupIntervalDays int `env:"BACKGROUND_JOB_CLEANUP_INTERVAL_DAYS" envDefault:"7"`
}

// NewConfig creates a new configuration with default values
Expand Down
11 changes: 11 additions & 0 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package database
import (
"context"
"errors"
"time"

"github.com/modelcontextprotocol/registry/internal/model"
)
Expand Down Expand Up @@ -32,6 +33,16 @@ type Database interface {
ImportSeed(ctx context.Context, seedFilePath string) error
// Close closes the database connection
Close() error

// Domain verification methods
// GetVerifiedDomains retrieves all domains that are currently verified
GetVerifiedDomains(ctx context.Context) ([]string, error)
// GetDomainVerification retrieves domain verification details
GetDomainVerification(ctx context.Context, domain string) (*model.DomainVerification, error)
// UpdateDomainVerification updates or creates domain verification record
UpdateDomainVerification(ctx context.Context, domainVerification *model.DomainVerification) error
// CleanupOldVerifications removes old verification records before the given time
CleanupOldVerifications(ctx context.Context, before time.Time) (int, error)
}

// ConnectionType represents the type of database connection
Expand Down
84 changes: 84 additions & 0 deletions internal/database/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
type MemoryDB struct {
entries map[string]*model.ServerDetail
domainVerifications map[string]*model.DomainVerification // key: domain
metadata map[string]*model.Metadata // key: serverID
mu sync.RWMutex
}

Expand All @@ -33,6 +34,7 @@ func NewMemoryDB(e map[string]*model.Server) *MemoryDB {
return &MemoryDB{
entries: serverDetails,
domainVerifications: make(map[string]*model.DomainVerification),
metadata: make(map[string]*model.Metadata),
}
}

Expand Down Expand Up @@ -356,3 +358,85 @@ func (db *MemoryDB) GetVerificationTokens(ctx context.Context, domain string) (*

return domainVerification.VerificationTokens, nil
}

// GetVerifiedDomains retrieves all domains that are currently verified
func (db *MemoryDB) GetVerifiedDomains(ctx context.Context) ([]string, error) {
db.mu.RLock()
defer db.mu.RUnlock()

var domains []string
for _, metadata := range db.metadata {
if metadata.DomainVerification != nil &&
metadata.DomainVerification.Status == model.VerificationStatusVerified {
domains = append(domains, metadata.DomainVerification.Domain)
}
}

return domains, nil
}

// GetDomainVerification retrieves domain verification details
func (db *MemoryDB) GetDomainVerification(ctx context.Context, domain string) (*model.DomainVerification, error) {
db.mu.RLock()
defer db.mu.RUnlock()

for _, metadata := range db.metadata {
if metadata.DomainVerification != nil &&
metadata.DomainVerification.Domain == domain {
return metadata.DomainVerification, nil
}
}

return nil, ErrNotFound
}

// UpdateDomainVerification updates or creates domain verification record
func (db *MemoryDB) UpdateDomainVerification(ctx context.Context, domainVerification *model.DomainVerification) error {
db.mu.Lock()
defer db.mu.Unlock()

// Find existing metadata entry for this domain or create a new one
var targetMetadata *model.Metadata
var targetServerID string

// Find existing metadata for this domain
for _, metadata := range db.metadata {
if metadata.DomainVerification != nil &&
metadata.DomainVerification.Domain == domainVerification.Domain {
targetMetadata = metadata
break
}
}

if targetMetadata == nil {
// Create new metadata entry
targetServerID = uuid.New().String()
targetMetadata = &model.Metadata{
ServerID: targetServerID,
}
db.metadata[targetServerID] = targetMetadata
}

targetMetadata.DomainVerification = domainVerification
return nil
}

// CleanupOldVerifications removes old verification records before the given time
func (db *MemoryDB) CleanupOldVerifications(ctx context.Context, before time.Time) (int, error) {
db.mu.Lock()
defer db.mu.Unlock()

count := 0
for serverID, metadata := range db.metadata {
if metadata.DomainVerification != nil {
// Remove records that are old and have failed status
if metadata.DomainVerification.Status == model.VerificationStatusFailed &&
metadata.DomainVerification.LastVerificationAttempt.Before(before) {
delete(db.metadata, serverID)
count++
}
}
}

return count, nil
}
98 changes: 98 additions & 0 deletions internal/database/mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type MongoDB struct {
database *mongo.Database
serverCollection *mongo.Collection
verificationCollection *mongo.Collection
metadataCollection *mongo.Collection
}

// NewMongoDB creates a new instance of the MongoDB database
Expand All @@ -40,6 +41,7 @@ func NewMongoDB(ctx context.Context, connectionURI, databaseName, collectionName
database := client.Database(databaseName)
serverCollection := database.Collection(collectionName)
verificationCollection := database.Collection(verificationCollectionName)
metadataCollection := database.Collection("metadata")

// Create indexes for better query performance
models := []mongo.IndexModel{
Expand Down Expand Up @@ -90,6 +92,7 @@ func NewMongoDB(ctx context.Context, connectionURI, databaseName, collectionName
database: database,
serverCollection: serverCollection,
verificationCollection: verificationCollection,
metadataCollection: metadataCollection,
}, nil
}

Expand Down Expand Up @@ -402,3 +405,98 @@ func (db *MongoDB) GetVerificationTokens(ctx context.Context, domain string) (*m

return domainVerification.VerificationTokens, nil
}

// GetVerifiedDomains retrieves all domains that are currently verified
func (db *MongoDB) GetVerifiedDomains(ctx context.Context) ([]string, error) {
filter := bson.M{
"domain_verification.status": model.VerificationStatusVerified,
}

cursor, err := db.metadataCollection.Find(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to query verified domains: %w", err)
}
defer cursor.Close(ctx)

var domains []string
for cursor.Next(ctx) {
var metadata model.Metadata
if err := cursor.Decode(&metadata); err != nil {
log.Printf("Failed to decode metadata: %v", err)
continue
}

if metadata.DomainVerification != nil {
domains = append(domains, metadata.DomainVerification.Domain)
}
}

if err := cursor.Err(); err != nil {
return nil, fmt.Errorf("cursor error: %w", err)
}

return domains, nil
}

// GetDomainVerification retrieves domain verification details
func (db *MongoDB) GetDomainVerification(ctx context.Context, domain string) (*model.DomainVerification, error) {
filter := bson.M{
"domain_verification.domain": domain,
}

var metadata model.Metadata
err := db.metadataCollection.FindOne(ctx, filter).Decode(&metadata)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to get domain verification: %w", err)
}

if metadata.DomainVerification == nil {
return nil, ErrNotFound
}

return metadata.DomainVerification, nil
}

// UpdateDomainVerification updates or creates domain verification record
func (db *MongoDB) UpdateDomainVerification(ctx context.Context, domainVerification *model.DomainVerification) error {
filter := bson.M{
"domain_verification.domain": domainVerification.Domain,
}

update := bson.M{
"$set": bson.M{
"domain_verification": domainVerification,
},
"$setOnInsert": bson.M{
"server_id": uuid.New().String(),
},
}

opts := options.Update().SetUpsert(true)
_, err := db.metadataCollection.UpdateOne(ctx, filter, update, opts)
if err != nil {
return fmt.Errorf("failed to update domain verification: %w", err)
}

return nil
}

// CleanupOldVerifications removes old verification records before the given time
func (db *MongoDB) CleanupOldVerifications(ctx context.Context, before time.Time) (int, error) {
filter := bson.M{
"domain_verification.status": model.VerificationStatusFailed,
"domain_verification.last_verification_attempt": bson.M{
"$lt": before,
},
}

result, err := db.metadataCollection.DeleteMany(ctx, filter)
if err != nil {
return 0, fmt.Errorf("failed to cleanup old verifications: %w", err)
}

return int(result.DeletedCount), nil
}
Loading