Init world. Basic registry API server
Work in progress . This commit adds the core registry service implementation with methods to: - Retrieve all registry entries - List entries with cursor-based pagination - Fetch specific server details by ID
This commit is contained in:
parent
2e8c761edf
commit
4fb5bc477e
24 changed files with 1702 additions and 2 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
build/*
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN go build -o /build/registry ./cmd/registry
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
COPY --from=builder /build/registry .
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./registry"]
|
||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Model Context Protocol
|
||||
Copyright (c) 2025 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
102
Makefile
Normal file
102
Makefile
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Makefile for MCP Registry project
|
||||
|
||||
# Go parameters
|
||||
BINARY_NAME=registry
|
||||
MAIN_PACKAGE=./cmd/registry
|
||||
BUILD_DIR=build
|
||||
GO=go
|
||||
|
||||
# Build settings
|
||||
VERSION=0.1.0
|
||||
GIT_COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
BUILD_TIME=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
LDFLAGS=-ldflags "-s -w -X main.Version=$(VERSION) -X main.GitCommit=$(GIT_COMMIT) -X main.BuildTime=$(BUILD_TIME)"
|
||||
|
||||
VERSION_INFO=Version: $(BINARY_NAME) v$(VERSION)
|
||||
|
||||
.PHONY: all build clean test run run-test lint fmt vet help
|
||||
|
||||
all: clean build
|
||||
|
||||
# Build the application
|
||||
build:
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@$(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE)
|
||||
@echo "Build complete!"
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
@rm -rf $(BUILD_DIR)
|
||||
@echo "Clean complete!"
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
@$(GO) test ./... -v
|
||||
@echo "Tests complete!"
|
||||
|
||||
# Run the application
|
||||
run:
|
||||
@echo "Running $(BINARY_NAME)..."
|
||||
@$(GO) run $(MAIN_PATH)
|
||||
|
||||
# Run the application with test environment (using fake service)
|
||||
run-test:
|
||||
@echo "Running $(BINARY_NAME) with test environment..."
|
||||
@APP_ENV=test $(GO) run $(MAIN_PATH)
|
||||
|
||||
# Install dependencies
|
||||
deps:
|
||||
@echo "Installing dependencies..."
|
||||
@$(GO) mod download
|
||||
@echo "Dependencies installed!"
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
@echo "Formatting code..."
|
||||
@$(GO) fmt ./...
|
||||
@echo "Formatting complete!"
|
||||
|
||||
# Run go vet
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
@$(GO) vet ./...
|
||||
@echo "Vet complete!"
|
||||
|
||||
# Run golint if installed
|
||||
lint:
|
||||
@if command -v golint > /dev/null; then \
|
||||
echo "Running golint..."; \
|
||||
golint ./...; \
|
||||
echo "Lint complete!"; \
|
||||
else \
|
||||
echo "golint not installed. Run: go install golang.org/x/lint/golint@latest"; \
|
||||
fi
|
||||
|
||||
# Build for multiple platforms (cross-compilation)
|
||||
build-all: clean
|
||||
@echo "Building for multiple platforms..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PATH)
|
||||
@GOOS=darwin GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PATH)
|
||||
@GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PATH)
|
||||
@GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PATH)
|
||||
@echo "Cross-compilation complete!"
|
||||
|
||||
# Show help
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " all - Clean and build the project"
|
||||
@echo " build - Build the project binary"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " test - Run all tests"
|
||||
@echo " run - Run the application"
|
||||
@echo " run-test - Run the application with test environment (fake service)"
|
||||
@echo " deps - Download dependencies"
|
||||
@echo " fmt - Format code using go fmt"
|
||||
@echo " vet - Run go vet"
|
||||
@echo " lint - Run golint (if installed)"
|
||||
@echo " build-all - Build for multiple platforms"
|
||||
@echo " help - Show this help message"
|
||||
206
README.md
206
README.md
|
|
@ -1 +1,205 @@
|
|||
# registry
|
||||
# MCP Registry
|
||||
|
||||
A reference implementation of a registry service for Model Context Protocol (MCP) servers.
|
||||
|
||||
> **Note:** This project is currently a work in progress.
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP Registry service provides a centralized repository for MCP server entries. It allows discovery and management of various MCP implementations with their associated metadata, configurations, and capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- RESTful API for managing MCP registry entries (list, get, create, update, delete)
|
||||
- Health check endpoint for service monitoring
|
||||
- Support for various environment configurations
|
||||
- Graceful shutdown handling
|
||||
- MongoDB and in-memory database support
|
||||
- Comprehensive API documentation
|
||||
- Pagination support for listing registry entries
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
├── cmd/ # Application entry points
|
||||
├── config/ # Configuration files
|
||||
├── internal/ # Private application code
|
||||
│ ├── api/ # HTTP server and request handlers
|
||||
│ ├── config/ # Configuration management
|
||||
│ ├── model/ # Data models
|
||||
│ └── service/ # Business logic
|
||||
├── pkg/ # Public libraries
|
||||
├── scripts/ # Utility scripts
|
||||
└── build/ # Build artifacts
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.18 or later
|
||||
- MongoDB (optional, for production use)
|
||||
- Docker (optional, for containerized deployment)
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
This will create the `mcp-registry` binary in the `build/` directory.
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
./build/mcp-registry
|
||||
```
|
||||
|
||||
Alternatively, run
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
By default, the service will run on `http://localhost:8080`.
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
You can build and run the service using Docker:
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
docker build -t mcp-registry .
|
||||
|
||||
# Run the container
|
||||
docker run -p 8080:8080 mcp-registry
|
||||
```
|
||||
|
||||
This will build and start the MCP Registry service in a container, exposing it on port 8080.
|
||||
|
||||
### Using the Fake Service
|
||||
|
||||
For development and testing purposes, the application includes a fake service with pre-populated registry entries. To use the fake service:
|
||||
|
||||
1. Set the environment to "test":
|
||||
|
||||
```bash
|
||||
export APP_ENV=test
|
||||
./build/mcp-registry
|
||||
```
|
||||
|
||||
Alternatively, run
|
||||
|
||||
```bash
|
||||
make run-test
|
||||
```
|
||||
|
||||
The fake service provides three sample MCP registry entries with the following capabilities:
|
||||
|
||||
- Registry 1: Code generation and completion capabilities
|
||||
- Registry 2: Chat and knowledge base capabilities
|
||||
- Registry 3: Data visualization and analysis capabilities
|
||||
|
||||
You can interact with the fake data through the API endpoints:
|
||||
|
||||
- List all entries: `GET /servers`
|
||||
|
||||
|
||||
The fake service is useful for:
|
||||
- Frontend development without a real backend
|
||||
- Testing API integrations
|
||||
- Example data structure reference
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Health Check
|
||||
|
||||
```
|
||||
GET /v0/health
|
||||
```
|
||||
|
||||
Returns the health status of the service:
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### Registry Endpoints
|
||||
|
||||
#### List Registry Server Entries
|
||||
|
||||
```
|
||||
GET /v0/servers
|
||||
```
|
||||
|
||||
Lists MCP registry server entries with pagination support.
|
||||
|
||||
Query parameters:
|
||||
- `limit`: Maximum number of entries to return (default: 30, max: 100)
|
||||
- `cursor`: Pagination cursor for retrieving next set of results
|
||||
|
||||
Response example:
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Example MCP Server",
|
||||
"description": "An example MCP server implementation",
|
||||
"url": "https://example.com/mcp",
|
||||
"repository": {
|
||||
"url": "https://github.com/example/mcp-server",
|
||||
"stars": 120
|
||||
},
|
||||
"version": "1.0.0",
|
||||
],
|
||||
"metadata": {
|
||||
"next_cursor": "cursor-value-for-next-page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ping Endpoint
|
||||
|
||||
```
|
||||
GET /v0/ping
|
||||
```
|
||||
|
||||
Simple ping endpoint that returns environment configuration information:
|
||||
```json
|
||||
{
|
||||
"environment": "dev",
|
||||
"version": "mcp-registry-<sha>"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
The service can be configured using environment variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `APP_ENV` | Application environment (production, test) | production |
|
||||
| `APP_VERSION`| Application version | dev |
|
||||
| `DATABASE_URL` | MongoDB connection string | mongodb://localhost:27017 |
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test script to validate API endpoints:
|
||||
|
||||
```bash
|
||||
./scripts/test_endpoints.sh
|
||||
```
|
||||
|
||||
You can specify specific endpoints to test:
|
||||
|
||||
```bash
|
||||
./scripts/test_endpoints.sh --endpoint health
|
||||
./scripts/test_endpoints.sh --endpoint servers
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
|
|
|
|||
101
cmd/registry/main.go
Normal file
101
cmd/registry/main.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/api"
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/database"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse command line flags
|
||||
showVersion := flag.Bool("version", false, "Display version information")
|
||||
flag.Parse()
|
||||
|
||||
// Show version information if requested
|
||||
if *showVersion {
|
||||
fmt.Printf("MCP Registry v%s\n", Version)
|
||||
fmt.Printf("Git commit: %s\n", GitCommit)
|
||||
fmt.Printf("Build time: %s\n", BuildTime)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Starting MCP Registry Application v%s (commit: %s)", Version, GitCommit)
|
||||
|
||||
// Initialize configuration
|
||||
cfg := config.NewConfig()
|
||||
|
||||
// Initialize services based on environment
|
||||
var registryService service.RegistryService
|
||||
|
||||
if cfg.Environment == "test" {
|
||||
// Use fake registry service in test environment
|
||||
registryService = service.NewFakeRegistryService()
|
||||
log.Println("Using fake registry service with dummy data (test environment)")
|
||||
} 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
|
||||
mongoDB, err := database.NewMongoDB(ctx, cfg.DatabaseURL, cfg.DatabaseName, cfg.CollectionName)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to MongoDB: %v", err)
|
||||
}
|
||||
|
||||
// Create registry service with MongoDB
|
||||
registryService = service.NewRegistryServiceWithDB(mongoDB)
|
||||
log.Printf("Using MongoDB-based registry service (%s environment)", cfg.Environment)
|
||||
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")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Initialize HTTP server
|
||||
server := api.NewServer(cfg, registryService)
|
||||
|
||||
// Start server in a goroutine so it doesn't block signal handling
|
||||
go func() {
|
||||
log.Printf("HTTP server listening on %s", cfg.ServerAddress)
|
||||
if err := server.Start(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for interrupt signal to gracefully shutdown the server
|
||||
quit := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
// Create context with timeout for shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Gracefully shutdown the server
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Server exiting")
|
||||
}
|
||||
13
cmd/registry/version.go
Normal file
13
cmd/registry/version.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
// Version info for the MCP Registry application
|
||||
var (
|
||||
// Version is the current version of the MCP Registry application
|
||||
Version = "0.1.0"
|
||||
|
||||
// BuildTime is the time at which the binary was built
|
||||
BuildTime = "undefined"
|
||||
|
||||
// GitCommit is the git commit that was compiled
|
||||
GitCommit = "undefined"
|
||||
)
|
||||
21
go.mod
Normal file
21
go.mod
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
module github.com/modelcontextprotocol/registry
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
go.mongodb.org/mongo-driver v1.17.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
)
|
||||
52
go.sum
Normal file
52
go.sum
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
15
internal/api/handlers/v0/health.go
Normal file
15
internal/api/handlers/v0/health.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Package v0 contains API handlers for version 0 of the API
|
||||
package v0
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HealthHandler returns a handler for health check endpoint
|
||||
func HealthHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
27
internal/api/handlers/v0/ping.go
Normal file
27
internal/api/handlers/v0/ping.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Package v0 contains API handlers for version 0 of the API
|
||||
package v0
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
)
|
||||
|
||||
// PingHandler returns a handler for the ping endpoint that returns build version
|
||||
func PingHandler(cfg *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]string{
|
||||
"status": "ok",
|
||||
"version": cfg.Version,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
}
|
||||
125
internal/api/handlers/v0/servers.go
Normal file
125
internal/api/handlers/v0/servers.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// Package v0 contains API handlers for version 0 of the API
|
||||
package v0
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/modelcontextprotocol/registry/internal/model"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
)
|
||||
|
||||
// Response is a paginated API response
|
||||
type PaginatedResponse struct {
|
||||
Data []model.Entry `json:"servers"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// Metadata contains pagination metadata
|
||||
type Metadata struct {
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
|
||||
// ServersHandler returns a handler for listing registry items
|
||||
func ServersHandler(registry service.RegistryService) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse cursor and limit from query parameters
|
||||
cursor := r.URL.Query().Get("cursor")
|
||||
if cursor != "" {
|
||||
_, err := uuid.Parse(cursor)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid cursor parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
|
||||
// Default limit if not specified
|
||||
limit := 30
|
||||
|
||||
// Try to parse limit from query param
|
||||
if limitStr != "" {
|
||||
parsedLimit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid limit parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if limit is within reasonable bounds
|
||||
if parsedLimit <= 0 {
|
||||
http.Error(w, "Limit must be greater than 0", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedLimit > 100 {
|
||||
// Cap maximum limit to prevent excessive queries
|
||||
limit = 100
|
||||
} else {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
// Use the GetAll method to get paginated results
|
||||
registries, nextCursor, err := registry.List(cursor, limit)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create paginated response
|
||||
response := PaginatedResponse{
|
||||
Data: registries,
|
||||
}
|
||||
|
||||
// Add metadata if there's a next cursor
|
||||
if nextCursor != "" {
|
||||
response.Metadata = Metadata{
|
||||
NextCursor: nextCursor,
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
}
|
||||
|
||||
// ServersDetailHandler returns a handler for getting details of a specific server by ID
|
||||
func ServersDetailHandler(registry service.RegistryService) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the server ID from the URL path
|
||||
id := r.PathValue("id")
|
||||
|
||||
// Validate that the ID is a valid UUID
|
||||
_, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid server ID format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the server details from the registry service
|
||||
serverDetail, err := registry.GetByID(id)
|
||||
if err != nil {
|
||||
if err.Error() == "record not found" {
|
||||
http.Error(w, "Server not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Error retrieving server details", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(serverDetail)
|
||||
}
|
||||
}
|
||||
19
internal/api/router/router.go
Normal file
19
internal/api/router/router.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Package router contains API routing logic
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
)
|
||||
|
||||
// New creates a new router with all API versions registered
|
||||
func New(cfg *config.Config, registry service.RegistryService) *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Register routes for all API versions
|
||||
RegisterV0Routes(mux, cfg, registry)
|
||||
|
||||
return mux
|
||||
}
|
||||
19
internal/api/router/v0.go
Normal file
19
internal/api/router/v0.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Package router contains API routing logic
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
)
|
||||
|
||||
// RegisterV0Routes registers all v0 API routes to the provided router
|
||||
func RegisterV0Routes(mux *http.ServeMux, cfg *config.Config, registry service.RegistryService) {
|
||||
// Register v0 endpoints
|
||||
mux.HandleFunc("/v0/health", v0.HealthHandler())
|
||||
mux.HandleFunc("/v0/servers", v0.ServersHandler(registry))
|
||||
mux.HandleFunc("/v0/servers/{id}", v0.ServersDetailHandler(registry))
|
||||
mux.HandleFunc("/v0/ping", v0.PingHandler(cfg))
|
||||
}
|
||||
48
internal/api/server.go
Normal file
48
internal/api/server.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/api/router"
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
)
|
||||
|
||||
// Server represents the HTTP server
|
||||
type Server struct {
|
||||
config *config.Config
|
||||
registry service.RegistryService
|
||||
router *http.ServeMux
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// NewServer creates a new HTTP server
|
||||
func NewServer(cfg *config.Config, registryService service.RegistryService) *Server {
|
||||
// Create router with all API versions registered
|
||||
mux := router.New(cfg, registryService)
|
||||
|
||||
server := &Server{
|
||||
config: cfg,
|
||||
registry: registryService,
|
||||
router: mux,
|
||||
server: &http.Server{
|
||||
Addr: cfg.ServerAddress,
|
||||
Handler: mux,
|
||||
},
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// Start begins listening for incoming HTTP requests
|
||||
func (s *Server) Start() error {
|
||||
log.Printf("HTTP server starting on %s", s.config.ServerAddress)
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the server
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
56
internal/config/config.go
Normal file
56
internal/config/config.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
// Config holds the application configuration
|
||||
type Config struct {
|
||||
ServerAddress string
|
||||
DatabaseURL string
|
||||
DatabaseName string
|
||||
CollectionName string
|
||||
LogLevel string
|
||||
Environment string // Added environment field
|
||||
Version string // Application version
|
||||
}
|
||||
|
||||
// NewConfig creates a new configuration with default values
|
||||
func NewConfig() *Config {
|
||||
// Get environment from OS environment variable or default to "production"
|
||||
env := os.Getenv("APP_ENV")
|
||||
if env == "" {
|
||||
env = "production"
|
||||
}
|
||||
|
||||
// Get version from environment variable or default to "dev"
|
||||
version := os.Getenv("APP_VERSION")
|
||||
if version == "" {
|
||||
version = "dev"
|
||||
}
|
||||
|
||||
// Get database URL from environment variable or default to localhost
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
dbURL = "localhost:5432"
|
||||
}
|
||||
|
||||
// Get database name from environment variable or default to "mcp_registry"
|
||||
dbName := os.Getenv("DATABASE_NAME")
|
||||
if dbName == "" {
|
||||
dbName = "mcp-registry"
|
||||
}
|
||||
// Get collection name from environment variable or default to "entries"
|
||||
collectionName := os.Getenv("COLLECTION_NAME")
|
||||
if collectionName == "" {
|
||||
collectionName = "servers_v2"
|
||||
}
|
||||
|
||||
return &Config{
|
||||
ServerAddress: ":8080",
|
||||
DatabaseURL: dbURL,
|
||||
DatabaseName: dbName,
|
||||
CollectionName: collectionName,
|
||||
LogLevel: "info",
|
||||
Environment: env,
|
||||
Version: version,
|
||||
}
|
||||
}
|
||||
48
internal/database/database.go
Normal file
48
internal/database/database.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/model"
|
||||
)
|
||||
|
||||
// Common database errors
|
||||
var (
|
||||
ErrNotFound = errors.New("record not found")
|
||||
ErrAlreadyExists = errors.New("record already exists")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrDatabase = errors.New("database error")
|
||||
)
|
||||
|
||||
// Database defines the interface for database operations on MCPRegistry entries
|
||||
type Database interface {
|
||||
// List retrieves all MCPRegistry entries with optional filtering
|
||||
List(ctx context.Context, filter map[string]interface{}, cursor string, limit int) ([]*model.Entry, string, error)
|
||||
// GetByID retrieves a single ServerDetail by it's ID
|
||||
GetByID(ctx context.Context, id string) (*model.ServerDetail, error)
|
||||
// Close closes the database connection
|
||||
Close() error
|
||||
}
|
||||
|
||||
// ConnectionType represents the type of database connection
|
||||
type ConnectionType string
|
||||
|
||||
const (
|
||||
// ConnectionTypeMemory represents an in-memory database connection
|
||||
ConnectionTypeMemory ConnectionType = "memory"
|
||||
// ConnectionTypeMongoDB represents a MongoDB database connection
|
||||
ConnectionTypeMongoDB ConnectionType = "mongodb"
|
||||
)
|
||||
|
||||
// ConnectionInfo provides information about the database connection
|
||||
type ConnectionInfo struct {
|
||||
// Type indicates the type of database connection
|
||||
Type ConnectionType
|
||||
// IsConnected indicates whether the database is currently connected
|
||||
IsConnected bool
|
||||
// Raw provides access to the underlying connection object, which will vary by implementation
|
||||
// For MongoDB, this will be *mongo.Client
|
||||
// For MemoryDB, this will be map[string]*model.MCPRegistry
|
||||
Raw any
|
||||
}
|
||||
152
internal/database/memory.go
Normal file
152
internal/database/memory.go
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/model"
|
||||
)
|
||||
|
||||
// MemoryDB is an in-memory implementation of the Database interface
|
||||
type MemoryDB struct {
|
||||
entries map[string]*model.Entry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMemoryDB creates a new instance of the in-memory database
|
||||
func NewMemoryDB(e map[string]*model.Entry) *MemoryDB {
|
||||
return &MemoryDB{
|
||||
entries: e,
|
||||
}
|
||||
}
|
||||
|
||||
// List retrieves all MCPRegistry entries with optional filtering and pagination
|
||||
func (db *MemoryDB) List(ctx context.Context, filter map[string]interface{}, cursor string, limit int) ([]*model.Entry, 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 []*model.Entry
|
||||
for _, entry := range db.entries {
|
||||
entryCopy := *entry
|
||||
allEntries = append(allEntries, &entryCopy)
|
||||
}
|
||||
|
||||
// Simple filtering implementation
|
||||
var filteredEntries []*model.Entry
|
||||
for _, entry := range allEntries {
|
||||
include := true
|
||||
|
||||
// Apply filters if any
|
||||
if filter != nil {
|
||||
for key, value := range filter {
|
||||
switch key {
|
||||
case "name":
|
||||
if entry.Name != value.(string) {
|
||||
include = false
|
||||
}
|
||||
case "repoUrl":
|
||||
if entry.Repository.URL != value.(string) {
|
||||
include = false
|
||||
}
|
||||
case "serverDetail.id":
|
||||
if entry.ID != value.(string) {
|
||||
include = false
|
||||
}
|
||||
case "version":
|
||||
if entry.Version != value.(string) {
|
||||
include = false
|
||||
}
|
||||
// Add more filter options as needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if include {
|
||||
filteredEntries = append(filteredEntries, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Find starting point for cursor-based pagination
|
||||
startIdx := 0
|
||||
if cursor != "" {
|
||||
for i, entry := range filteredEntries {
|
||||
if entry.ID == cursor {
|
||||
startIdx = i + 1 // Start after the cursor
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort filteredEntries by ID for consistent pagination
|
||||
sort.Slice(filteredEntries, func(i, j int) bool {
|
||||
return filteredEntries[i].ID < filteredEntries[j].ID
|
||||
})
|
||||
|
||||
// Apply pagination
|
||||
endIdx := startIdx + limit
|
||||
if endIdx > len(filteredEntries) {
|
||||
endIdx = len(filteredEntries)
|
||||
}
|
||||
|
||||
var result []*model.Entry
|
||||
if startIdx < len(filteredEntries) {
|
||||
result = filteredEntries[startIdx:endIdx]
|
||||
} else {
|
||||
result = []*model.Entry{}
|
||||
}
|
||||
|
||||
// Determine next cursor
|
||||
nextCursor := ""
|
||||
if endIdx < len(filteredEntries) {
|
||||
nextCursor = filteredEntries[endIdx-1].ID
|
||||
}
|
||||
|
||||
return result, nextCursor, nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a single ServerDetail by its ID
|
||||
func (db *MemoryDB) GetByID(ctx context.Context, id string) (*model.ServerDetail, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
|
||||
if entry, exists := db.entries[id]; exists {
|
||||
return &model.ServerDetail{
|
||||
ID: entry.ID,
|
||||
Name: entry.Name,
|
||||
Description: entry.Description,
|
||||
Version: entry.Version,
|
||||
Repository: entry.Repository,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
// For an in-memory database, this is a no-op
|
||||
func (db *MemoryDB) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connection returns information about the database connection
|
||||
func (db *MemoryDB) Connection() *ConnectionInfo {
|
||||
return &ConnectionInfo{
|
||||
Type: ConnectionTypeMemory,
|
||||
IsConnected: true, // Memory DB is always connected
|
||||
Raw: db.entries,
|
||||
}
|
||||
}
|
||||
189
internal/database/mongo.go
Normal file
189
internal/database/mongo.go
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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"
|
||||
)
|
||||
|
||||
// MongoDB is an implementation of the Database interface using MongoDB
|
||||
type MongoDB struct {
|
||||
client *mongo.Client
|
||||
database *mongo.Database
|
||||
collection *mongo.Collection
|
||||
}
|
||||
|
||||
// NewMongoDB creates a new instance of the MongoDB database
|
||||
func NewMongoDB(ctx context.Context, connectionURI, databaseName, collectionName string) (*MongoDB, error) {
|
||||
// Set client options and connect to MongoDB
|
||||
clientOptions := options.Client().ApplyURI(connectionURI)
|
||||
client, err := mongo.Connect(ctx, clientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ping the MongoDB server to verify the connection
|
||||
if err = client.Ping(ctx, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get database and collection
|
||||
database := client.Database(databaseName)
|
||||
collection := database.Collection(collectionName)
|
||||
|
||||
// Create indexes for better query performance
|
||||
models := []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "name", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
}
|
||||
|
||||
_, err = collection.Indexes().CreateMany(ctx, models)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MongoDB{
|
||||
client: client,
|
||||
database: database,
|
||||
collection: collection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List retrieves MCPRegistry entries with optional filtering and pagination
|
||||
func (db *MongoDB) List(ctx context.Context, filter map[string]interface{}, cursor string, limit int) ([]*model.Entry, string, error) {
|
||||
if limit <= 0 {
|
||||
// Set default limit if not provided
|
||||
limit = 10
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return nil, "", ctx.Err()
|
||||
}
|
||||
|
||||
// Convert Go map to MongoDB filter
|
||||
mongoFilter := bson.M{}
|
||||
// Map common filter keys to MongoDB document paths
|
||||
for k, v := range filter {
|
||||
// Handle nested fields with dot notation
|
||||
switch k {
|
||||
case "publisher.trusted":
|
||||
mongoFilter["publisher.trusted"] = v
|
||||
default:
|
||||
mongoFilter[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Setup pagination options
|
||||
findOptions := options.Find()
|
||||
|
||||
// If cursor is provided, add condition to filter to only get records after the cursor
|
||||
if cursor != "" {
|
||||
// Validate that the cursor is a valid UUID
|
||||
if _, err := uuid.Parse(cursor); err != nil {
|
||||
return nil, "", fmt.Errorf("invalid cursor format: %w", err)
|
||||
}
|
||||
|
||||
// Fetch the document at the cursor to get its sort values
|
||||
var cursorDoc model.Entry
|
||||
err := db.collection.FindOne(ctx, bson.M{"id": cursor}).Decode(&cursorDoc)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
// If cursor document not found, start from beginning
|
||||
} else {
|
||||
return nil, "", err
|
||||
}
|
||||
} else {
|
||||
// Use the cursor document's ID to paginate (records with ID > cursor's ID)
|
||||
mongoFilter["id"] = bson.M{"$gt": cursor}
|
||||
}
|
||||
}
|
||||
|
||||
// Set sort order by ID (for consistent pagination)
|
||||
findOptions.SetSort(bson.M{"id": 1})
|
||||
|
||||
// Set limit if provided and valid
|
||||
if limit > 0 {
|
||||
findOptions.SetLimit(int64(limit))
|
||||
}
|
||||
|
||||
// Execute find operation with options
|
||||
mongoCursor, err := db.collection.Find(ctx, mongoFilter, findOptions)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer mongoCursor.Close(ctx)
|
||||
|
||||
// Decode results
|
||||
var results []*model.Entry
|
||||
if err = mongoCursor.All(ctx, &results); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Determine the next cursor
|
||||
nextCursor := ""
|
||||
if len(results) > 0 && limit > 0 && len(results) >= limit {
|
||||
// Use the last item's ID as the next cursor
|
||||
nextCursor = results[len(results)-1].ID
|
||||
}
|
||||
|
||||
return results, nextCursor, nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a single ServerDetail by its ID
|
||||
func (db *MongoDB) GetByID(ctx context.Context, id string) (*model.ServerDetail, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Create a filter for the ID
|
||||
filter := bson.M{"id": id}
|
||||
|
||||
// Find the entry in the database
|
||||
var entry model.ServerDetail
|
||||
err := db.collection.FindOne(ctx, filter).Decode(&entry)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("error retrieving entry: %w", err)
|
||||
}
|
||||
|
||||
// Create and return a ServerDetail from the entry data
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *MongoDB) Close() error {
|
||||
return db.client.Disconnect(context.Background())
|
||||
}
|
||||
|
||||
// Connection returns information about the database connection
|
||||
func (db *MongoDB) Connection() *ConnectionInfo {
|
||||
isConnected := false
|
||||
// Check if the client is connected
|
||||
if db.client != nil {
|
||||
// A quick ping with 1 second timeout to verify connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
err := db.client.Ping(ctx, nil)
|
||||
isConnected = (err == nil)
|
||||
}
|
||||
|
||||
return &ConnectionInfo{
|
||||
Type: ConnectionTypeMongoDB,
|
||||
IsConnected: isConnected,
|
||||
Raw: db.client,
|
||||
}
|
||||
}
|
||||
79
internal/model/model.go
Normal file
79
internal/model/model.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package model
|
||||
|
||||
type Entry struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Repository Repository `json:"repository,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
SubFolder string `json:"subfolder,omitempty"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
}
|
||||
|
||||
type ServerDetail struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Repository Repository `json:"repository,omitempty"`
|
||||
RegistryCanonical string `json:"registry_canonical,omitempty"`
|
||||
Registries []Registries `json:"registries,omitempty"`
|
||||
Remotes []Remotes `json:"remotes,omitempty"`
|
||||
}
|
||||
|
||||
type Registries struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
PackageName string `json:"package_name,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
CommandArguments Command `json:"command_arguments,omitempty"`
|
||||
}
|
||||
|
||||
type Remotes struct {
|
||||
TransportType string `json:"transport_type,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
SubCommands []SubCommand `json:"sub_commands,omitempty"`
|
||||
PositionalArguments []PositionalArgument `json:"positional_arguments,omitempty"`
|
||||
EnvironmentVariables []EnvironmentVariable `json:"environment_variables,omitempty"`
|
||||
NamedArguments []NamedArguments `json:"named_arguments,omitempty"`
|
||||
}
|
||||
|
||||
type EnvironmentVariable struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
type Argument struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DefaultValue string `json:"default_value,omitempty"`
|
||||
IsRequired bool `json:"is_required,omitempty"`
|
||||
IsEditable bool `json:"is_editable,omitempty"`
|
||||
IsRepeatable bool `json:"is_repeatable,omitempty"`
|
||||
Choices []string `json:"choices,omitempty"`
|
||||
}
|
||||
|
||||
type PositionalArgument struct {
|
||||
Position int `json:"position,omitempty"`
|
||||
Argument Argument `json:"argument,omitempty"`
|
||||
}
|
||||
|
||||
type SubCommand struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
NamedArguments []NamedArguments `json:"named_arguments,omitempty"`
|
||||
}
|
||||
type NamedArguments struct {
|
||||
ShortFlag string `json:"short_flag,omitempty"`
|
||||
LongFlag string `json:"long_flag,omitempty"`
|
||||
RequiresValue bool `json:"requires_value,omitempty"`
|
||||
Argument Argument `json:"argument,omitempty"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue