feat: load seed.json servers by default
This commit is contained in:
parent
f843bd4243
commit
acdf3efeaf
5 changed files with 45 additions and 154 deletions
|
|
@ -6,6 +6,7 @@ RUN go build -o /build/registry ./cmd/registry
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /build/registry .
|
COPY --from=builder /build/registry .
|
||||||
|
COPY --from=builder /app/data/seed.json /app/data/seed.json
|
||||||
COPY --from=builder /app/internal/docs/swagger.yaml /app/internal/docs/swagger.yaml
|
COPY --from=builder /app/internal/docs/swagger.yaml /app/internal/docs/swagger.yaml
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,36 +39,35 @@ func main() {
|
||||||
// Initialize services based on environment
|
// Initialize services based on environment
|
||||||
var registryService service.RegistryService
|
var registryService service.RegistryService
|
||||||
|
|
||||||
if cfg.Environment == "test" {
|
// Use MongoDB for real registry service in production/other environments
|
||||||
// Use fake registry service in test environment
|
// Create a context with timeout for MongoDB connection
|
||||||
registryService = service.NewFakeRegistryService()
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
log.Println("Using fake registry service with dummy data (test environment)")
|
defer cancel()
|
||||||
} else {
|
|
||||||
// Use MongoDB for real registry service in production/other environments
|
|
||||||
// Create a context with timeout for MongoDB connection
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Connect to MongoDB
|
// Connect to MongoDB
|
||||||
mongoDB, err := database.NewMongoDB(ctx, cfg.DatabaseURL, cfg.DatabaseName, cfg.CollectionName)
|
mongoDB, err := database.NewMongoDB(ctx, cfg.DatabaseURL, cfg.DatabaseName, cfg.CollectionName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to connect to MongoDB: %v", err)
|
log.Fatalf("Failed to connect to MongoDB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create registry service with MongoDB
|
||||||
|
registryService = service.NewRegistryServiceWithDB(mongoDB)
|
||||||
|
log.Printf("MongoDB database name: %s", cfg.DatabaseName)
|
||||||
|
log.Printf("MongoDB collection name: %s", cfg.CollectionName)
|
||||||
|
|
||||||
|
// Store the MongoDB instance for later cleanup
|
||||||
|
defer func() {
|
||||||
|
if err := mongoDB.Close(); err != nil {
|
||||||
|
log.Printf("Error closing MongoDB connection: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("MongoDB connection closed successfully")
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Create registry service with MongoDB
|
if cfg.Import {
|
||||||
registryService = service.NewRegistryServiceWithDB(mongoDB)
|
log.Println("Importing data...")
|
||||||
log.Printf("Using MongoDB-based registry service (%s environment)", cfg.Environment)
|
database.ImportSeedFile(mongoDB, cfg.SeedFilePath)
|
||||||
log.Printf("MongoDB database name: %s", cfg.DatabaseName)
|
log.Println("Data import completed successfully")
|
||||||
log.Printf("MongoDB collection name: %s", cfg.CollectionName)
|
|
||||||
|
|
||||||
// Store the MongoDB instance for later cleanup
|
|
||||||
defer func() {
|
|
||||||
if err := mongoDB.Close(); err != nil {
|
|
||||||
log.Printf("Error closing MongoDB connection: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Println("MongoDB connection closed successfully")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize authentication services
|
// Initialize authentication services
|
||||||
|
|
@ -92,11 +91,11 @@ func main() {
|
||||||
log.Println("Shutting down server...")
|
log.Println("Shutting down server...")
|
||||||
|
|
||||||
// Create context with timeout for shutdown
|
// Create context with timeout for shutdown
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
sctx, scancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer scancel()
|
||||||
|
|
||||||
// Gracefully shutdown the server
|
// Gracefully shutdown the server
|
||||||
if err := server.Shutdown(ctx); err != nil {
|
if err := server.Shutdown(sctx); err != nil {
|
||||||
log.Fatalf("Server forced to shutdown: %v", err)
|
log.Fatalf("Server forced to shutdown: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,16 @@ import (
|
||||||
|
|
||||||
// Config holds the application configuration
|
// Config holds the application configuration
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"`
|
ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"`
|
||||||
DatabaseURL string `env:"DATABASE_URL" envDefault:"mongodb://localhost:27017"`
|
DatabaseURL string `env:"DATABASE_URL" envDefault:"mongodb://localhost:27017"`
|
||||||
DatabaseName string `env:"DATABASE_NAME" envDefault:"mcp-registry"`
|
DatabaseName string `env:"DATABASE_NAME" envDefault:"mcp-registry"`
|
||||||
CollectionName string `env:"COLLECTION_NAME" envDefault:"servers_v2"`
|
CollectionName string `env:"COLLECTION_NAME" envDefault:"servers_v2"`
|
||||||
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
|
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
|
||||||
Environment string `env:"ENVIRONMENT" envDefault:"production"`
|
SeedFilePath string `env:"SEED_FILE_PATH" envDefault:"data/seed.json"`
|
||||||
Version string `env:"VERSION" envDefault:"dev"`
|
Version string `env:"VERSION" envDefault:"dev"`
|
||||||
|
|
||||||
// GitHub OAuth settings
|
|
||||||
GithubClientID string `env:"GITHUB_CLIENT_ID" envDefault:""`
|
GithubClientID string `env:"GITHUB_CLIENT_ID" envDefault:""`
|
||||||
GithubClientSecret string `env:"GITHUB_CLIENT_SECRET" envDefault:""`
|
GithubClientSecret string `env:"GITHUB_CLIENT_SECRET" envDefault:""`
|
||||||
|
Import bool `env:"IMPORT" envDefault:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig creates a new configuration with default values
|
// NewConfig creates a new configuration with default values
|
||||||
|
|
|
||||||
313
internal/database/import.go
Normal file
313
internal/database/import.go
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/modelcontextprotocol/registry/internal/model"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImportSeedFile populates the MongoDB database with initial data from a seed file.
|
||||||
|
func ImportSeedFile(mongo *MongoDB, seedFilePath string) error {
|
||||||
|
// Set default seed file path if not provided
|
||||||
|
if seedFilePath == "" {
|
||||||
|
// Try to find the seed.json in the data directory
|
||||||
|
seedFilePath = filepath.Join("data", "seed.json")
|
||||||
|
if _, err := os.Stat(seedFilePath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("seed file not found at %s", seedFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a context with timeout for the database operations
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Read the seed file
|
||||||
|
seedData, err := readSeedFile(seedFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to read seed file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := mongo.collection
|
||||||
|
importData(ctx, collection, seedData)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readSeedFile reads and parses the seed.json file
|
||||||
|
func readSeedFile(path string) ([]model.ServerDetail, error) {
|
||||||
|
log.Printf("Reading seed file from %s", path)
|
||||||
|
|
||||||
|
// Read the file content
|
||||||
|
fileContent, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON content
|
||||||
|
var servers []model.ServerDetail
|
||||||
|
if err := json.Unmarshal(fileContent, &servers); err != nil {
|
||||||
|
// Try parsing as a raw JSON array and then convert to our model
|
||||||
|
var rawData []map[string]interface{}
|
||||||
|
if jsonErr := json.Unmarshal(fileContent, &rawData); jsonErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse JSON: %v (original error: %v)", jsonErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert raw data to model format
|
||||||
|
servers = convertToServerDetails(rawData)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Found %d server entries in seed file", len(servers))
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToServerDetails converts raw JSON data to ServerDetail structs
|
||||||
|
func convertToServerDetails(rawData []map[string]interface{}) []model.ServerDetail {
|
||||||
|
servers := make([]model.ServerDetail, 0, len(rawData))
|
||||||
|
|
||||||
|
for _, data := range rawData {
|
||||||
|
server := model.ServerDetail{}
|
||||||
|
|
||||||
|
// Extract basic fields
|
||||||
|
if id, ok := data["id"].(string); ok {
|
||||||
|
server.ID = id
|
||||||
|
}
|
||||||
|
if name, ok := data["name"].(string); ok {
|
||||||
|
server.Name = name
|
||||||
|
}
|
||||||
|
if desc, ok := data["description"].(string); ok {
|
||||||
|
server.Description = desc
|
||||||
|
}
|
||||||
|
if version, ok := data["version"].(string); ok {
|
||||||
|
if version == "" || version == "latest" {
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
server.VersionDetail = model.VersionDetail{
|
||||||
|
Version: version,
|
||||||
|
ReleaseDate: time.Now().Format(time.RFC3339),
|
||||||
|
IsLatest: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract repository
|
||||||
|
if repo, ok := data["repository"].(map[string]interface{}); ok {
|
||||||
|
server.Repository = model.Repository{
|
||||||
|
URL: getStringValue(repo, "url"),
|
||||||
|
SubFolder: getStringValue(repo, "subfolder"),
|
||||||
|
Branch: getStringValue(repo, "branch"),
|
||||||
|
Commit: getStringValue(repo, "commit"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract registries
|
||||||
|
if registries, ok := data["registries"].([]interface{}); ok {
|
||||||
|
for _, reg := range registries {
|
||||||
|
if regMap, ok := reg.(map[string]interface{}); ok {
|
||||||
|
registry := model.Registries{
|
||||||
|
Name: getStringValue(regMap, "name"),
|
||||||
|
PackageName: getStringValue(regMap, "packagename"),
|
||||||
|
License: getStringValue(regMap, "license"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle command arguments if present
|
||||||
|
if cmd, ok := regMap["command_arguments"].(map[string]interface{}); ok {
|
||||||
|
commandArgs := model.Command{}
|
||||||
|
|
||||||
|
// Extract sub commands
|
||||||
|
if subCmds, ok := cmd["sub_commands"].([]interface{}); ok {
|
||||||
|
for _, sc := range subCmds {
|
||||||
|
if scMap, ok := sc.(map[string]interface{}); ok {
|
||||||
|
subCmd := model.SubCommand{
|
||||||
|
Name: getStringValue(scMap, "name"),
|
||||||
|
Description: getStringValue(scMap, "description"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract named arguments in sub commands
|
||||||
|
if namedArgs, ok := scMap["named_arguments"].([]interface{}); ok {
|
||||||
|
for _, na := range namedArgs {
|
||||||
|
if naMap, ok := na.(map[string]interface{}); ok {
|
||||||
|
namedArg := extractNamedArgument(naMap)
|
||||||
|
subCmd.NamedArguments = append(subCmd.NamedArguments, namedArg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commandArgs.SubCommands = append(commandArgs.SubCommands, subCmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract positional arguments
|
||||||
|
if posArgs, ok := cmd["positional_arguments"].([]interface{}); ok {
|
||||||
|
for _, pa := range posArgs {
|
||||||
|
if paMap, ok := pa.(map[string]interface{}); ok {
|
||||||
|
posArg := model.PositionalArgument{}
|
||||||
|
|
||||||
|
if pos, ok := paMap["position"].(float64); ok {
|
||||||
|
posArg.Position = int(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg, ok := paMap["argument"].(map[string]interface{}); ok {
|
||||||
|
posArg.Argument = extractArgument(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
commandArgs.PositionalArguments = append(commandArgs.PositionalArguments, posArg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract environment variables
|
||||||
|
if envVars, ok := cmd["environment_variables"].([]interface{}); ok {
|
||||||
|
for _, ev := range envVars {
|
||||||
|
if evMap, ok := ev.(map[string]interface{}); ok {
|
||||||
|
envVar := model.EnvironmentVariable{
|
||||||
|
Name: getStringValue(evMap, "name"),
|
||||||
|
Description: getStringValue(evMap, "description"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if required, ok := evMap["required"].(bool); ok {
|
||||||
|
envVar.Required = required
|
||||||
|
}
|
||||||
|
|
||||||
|
commandArgs.EnvironmentVariables = append(commandArgs.EnvironmentVariables, envVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract named arguments
|
||||||
|
if namedArgs, ok := cmd["named_arguments"].([]interface{}); ok {
|
||||||
|
for _, na := range namedArgs {
|
||||||
|
if naMap, ok := na.(map[string]interface{}); ok {
|
||||||
|
namedArg := extractNamedArgument(naMap)
|
||||||
|
commandArgs.NamedArguments = append(commandArgs.NamedArguments, namedArg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.CommandArguments = commandArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Registries = append(server.Registries, registry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract remotes
|
||||||
|
if remotes, ok := data["remotes"].([]interface{}); ok {
|
||||||
|
for _, rem := range remotes {
|
||||||
|
if remMap, ok := rem.(map[string]interface{}); ok {
|
||||||
|
remote := model.Remotes{
|
||||||
|
TransportType: getStringValue(remMap, "transport_type"),
|
||||||
|
Url: getStringValue(remMap, "url"),
|
||||||
|
}
|
||||||
|
server.Remotes = append(server.Remotes, remote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractArgument extracts an Argument struct from a map
|
||||||
|
func extractArgument(data map[string]interface{}) model.Argument {
|
||||||
|
arg := model.Argument{
|
||||||
|
Name: getStringValue(data, "name"),
|
||||||
|
Description: getStringValue(data, "description"),
|
||||||
|
DefaultValue: getStringValue(data, "default_value"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract boolean fields
|
||||||
|
if isRequired, ok := data["is_required"].(bool); ok {
|
||||||
|
arg.IsRequired = isRequired
|
||||||
|
}
|
||||||
|
if isEditable, ok := data["is_editable"].(bool); ok {
|
||||||
|
arg.IsEditable = isEditable
|
||||||
|
}
|
||||||
|
if isRepeatable, ok := data["is_repeatable"].(bool); ok {
|
||||||
|
arg.IsRepeatable = isRepeatable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract string array for choices
|
||||||
|
if choices, ok := data["choices"].([]interface{}); ok {
|
||||||
|
for _, choice := range choices {
|
||||||
|
if strChoice, ok := choice.(string); ok {
|
||||||
|
arg.Choices = append(arg.Choices, strChoice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractNamedArgument extracts a NamedArguments struct from a map
|
||||||
|
func extractNamedArgument(data map[string]interface{}) model.NamedArguments {
|
||||||
|
namedArg := model.NamedArguments{
|
||||||
|
ShortFlag: getStringValue(data, "short_flag"),
|
||||||
|
LongFlag: getStringValue(data, "long_flag"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if requiresValue, ok := data["requires_value"].(bool); ok {
|
||||||
|
namedArg.RequiresValue = requiresValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg, ok := data["argument"].(map[string]interface{}); ok {
|
||||||
|
namedArg.Argument = extractArgument(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return namedArg
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStringValue safely extracts a string value from a map
|
||||||
|
func getStringValue(data map[string]interface{}, key string) string {
|
||||||
|
if val, ok := data[key].(string); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// importData imports the seed data into MongoDB
|
||||||
|
func importData(ctx context.Context, collection *mongo.Collection, servers []model.ServerDetail) {
|
||||||
|
log.Printf("Importing %d servers into collection %s", len(servers), collection.Name())
|
||||||
|
|
||||||
|
for i, server := range servers {
|
||||||
|
// Create filter based on server ID
|
||||||
|
filter := bson.M{"id": server.ID}
|
||||||
|
|
||||||
|
if server.VersionDetail.Version == "" {
|
||||||
|
server.VersionDetail.Version = "0.0.1"
|
||||||
|
server.VersionDetail.ReleaseDate = time.Now().Format(time.RFC3339)
|
||||||
|
server.VersionDetail.IsLatest = true
|
||||||
|
|
||||||
|
}
|
||||||
|
// Create update document
|
||||||
|
update := bson.M{"$set": server}
|
||||||
|
|
||||||
|
// Use upsert to create if not exists or update if exists
|
||||||
|
opts := options.Update().SetUpsert(true)
|
||||||
|
result, err := collection.UpdateOne(ctx, filter, update, opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error importing server %s: %v", server.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.UpsertedCount > 0 {
|
||||||
|
log.Printf("[%d/%d] Created server: %s", i+1, len(servers), server.Name)
|
||||||
|
} else if result.ModifiedCount > 0 {
|
||||||
|
log.Printf("[%d/%d] Updated server: %s", i+1, len(servers), server.Name)
|
||||||
|
} else {
|
||||||
|
log.Printf("[%d/%d] Server already up to date: %s", i+1, len(servers), server.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Import completed successfully")
|
||||||
|
}
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# MCP Registry Importer Tool
|
|
||||||
|
|
||||||
This command-line tool imports server data from a JSON file into a MongoDB database for the Model Context Protocol Registry.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run main.go [options]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Options
|
|
||||||
|
|
||||||
- `-uri string`: MongoDB connection URI (default "mongodb://localhost:27017")
|
|
||||||
- `-db string`: MongoDB database name (default "mcp_registry")
|
|
||||||
- `-collection string`: MongoDB collection name (default "servers")
|
|
||||||
- `-seed string`: Path to seed.json file (default: looks for data/seed.json relative to current directory)
|
|
||||||
- `-drop`: Drop collection before importing (default false)
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
Import data using default settings:
|
|
||||||
```bash
|
|
||||||
go run main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
Import data into a specific MongoDB instance:
|
|
||||||
```bash
|
|
||||||
go run main.go -uri mongodb://username:password@example.com:27017 -db registry
|
|
||||||
```
|
|
||||||
|
|
||||||
Import data from a specific seed file and replace all existing data:
|
|
||||||
```bash
|
|
||||||
go run main.go -seed /path/to/custom-seed.json -drop
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To build the tool as an executable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go build -o registry-importer
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can run it directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./registry-importer [options]
|
|
||||||
```
|
|
||||||
Loading…
Reference in a new issue