diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..35410cac --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..522f775f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/registry.iml b/.idea/registry.iml new file mode 100644 index 00000000..5e764c4f --- /dev/null +++ b/.idea/registry.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index d7f32b01..4678fd96 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,19 @@ GET /v0/health Returns the health status of the service: ```json { - "status": "ok" + "status": "ok", + "github_client_id": "client_id_here", + "database": { + "status": "connected", + "type": "mongodb", + "collection_count": 1 + }, + "uptime": "2h30m15s", + "version": "v0.1.0", + "memory": { + "alloc": "2.1 MB", + "sys": "8.5 MB" + } } ``` diff --git a/cmd/registry/main.go b/cmd/registry/main.go index 387462da..c9ed0045 100644 --- a/cmd/registry/main.go +++ b/cmd/registry/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/modelcontextprotocol/registry/internal/api" + "github.com/modelcontextprotocol/registry/internal/appinfo" "github.com/modelcontextprotocol/registry/internal/auth" "github.com/modelcontextprotocol/registry/internal/config" "github.com/modelcontextprotocol/registry/internal/database" @@ -41,6 +42,7 @@ func main() { ) // Initialize configuration + appinfo.Initialize() cfg := config.NewConfig() // Initialize services based on environment @@ -96,7 +98,7 @@ func main() { authService := auth.NewAuthService(cfg) // Initialize HTTP server - server := api.NewServer(cfg, registryService, authService) + server := api.NewServer(cfg, registryService, authService, db) // Start server in a goroutine so it doesn't block signal handling go func() { diff --git a/internal/api/handlers/v0/health.go b/internal/api/handlers/v0/health.go index 3dd78924..d5a8951e 100644 --- a/internal/api/handlers/v0/health.go +++ b/internal/api/handlers/v0/health.go @@ -3,23 +3,69 @@ package v0 import ( "encoding/json" + "fmt" "net/http" + "runtime" + "github.com/modelcontextprotocol/registry/internal/appinfo" "github.com/modelcontextprotocol/registry/internal/config" + "github.com/modelcontextprotocol/registry/internal/database" ) +const ( + DBStatusConnected = "connected" + DBStatusDisconnected = "disconnected" +) + +type DatabaseType string type HealthResponse struct { - Status string `json:"status"` - GitHubClientID string `json:"github_client_id"` + Status string `json:"status"` + GitHubClientID string `json:"github_client_id"` + Database *DatabaseHealth `json:"database"` + Uptime string `json:"uptime"` + Version string `json:"version"` + Memory *MemoryStats `json:"memory"` +} + +type DatabaseHealth struct { + Status string `json:"status"` + Type DatabaseType `json:"type"` + CollectionCount int `json:"collection_count"` +} + +type MemoryStats struct { + Alloc string `json:"alloc"` + Sys string `json:"sys"` } // HealthHandler returns a handler for health check endpoint -func HealthHandler(cfg *config.Config) http.HandlerFunc { +func HealthHandler(cfg *config.Config, db database.Database) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") + connInfo := db.Connection() + dbStatus := DBStatusDisconnected + if connInfo.IsConnected { + dbStatus = DBStatusConnected + } + databaseHealth := DatabaseHealth{ + Status: dbStatus, + Type: DatabaseType(connInfo.Type), + CollectionCount: connInfo.CollectionCount, + } + + var m runtime.MemStats + runtime.ReadMemStats(&m) + if err := json.NewEncoder(w).Encode(HealthResponse{ Status: "ok", GitHubClientID: cfg.GithubClientID, + Database: &databaseHealth, + Uptime: appinfo.GetUptimeString(), + Version: cfg.Version, + Memory: &MemoryStats{ + Alloc: fmt.Sprintf("%.1f MB", float64(m.Alloc)/1024/1024), + Sys: fmt.Sprintf("%.1f MB", float64(m.Sys)/1024/1024), + }, }); err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) } diff --git a/internal/api/handlers/v0/health_test.go b/internal/api/handlers/v0/health_test.go index baae604e..96ccab4e 100644 --- a/internal/api/handlers/v0/health_test.go +++ b/internal/api/handlers/v0/health_test.go @@ -9,22 +9,62 @@ import ( v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" "github.com/modelcontextprotocol/registry/internal/config" + "github.com/modelcontextprotocol/registry/internal/database" + "github.com/modelcontextprotocol/registry/internal/model" "github.com/stretchr/testify/assert" ) +// Mock database for testing +// Supports passing dbType +type mockDatabase struct { + dbType string +} + +// Implement List method to satisfy database.Database interface +func (m *mockDatabase) List(ctx context.Context, filter map[string]any, cursor string, limit int) ([]*model.Server, string, error) { + return []*model.Server{}, "", nil +} + +func (m *mockDatabase) Connection() *database.ConnectionInfo { + return &database.ConnectionInfo{ + IsConnected: true, + Type: database.ConnectionType(m.dbType), + CollectionCount: 0, + } +} + +func (m *mockDatabase) GetByID(ctx context.Context, id string) (*model.ServerDetail, error) { + return nil, database.ErrNotFound +} + +// Implement Publish method to satisfy database.Database interface +func (m *mockDatabase) Publish(ctx context.Context, serverDetail *model.ServerDetail) error { + return nil +} + +func (m *mockDatabase) ImportSeed(ctx context.Context, seedFilePath string) error { + return nil +} + +func (m *mockDatabase) Close() error { + return nil +} + func TestHealthHandler(t *testing.T) { // Test cases testCases := []struct { name string config *config.Config + dbType string expectedStatus int expectedBody v0.HealthResponse }{ { - name: "returns health status with github client id", + name: "returns health status with github client id (memory)", config: &config.Config{ GithubClientID: "test-github-client-id", }, + dbType: "memory", expectedStatus: http.StatusOK, expectedBody: v0.HealthResponse{ Status: "ok", @@ -32,10 +72,35 @@ func TestHealthHandler(t *testing.T) { }, }, { - name: "works with empty github client id", + name: "returns health status with github client id (mongo)", + config: &config.Config{ + GithubClientID: "test-github-client-id", + }, + dbType: "mongo", + expectedStatus: http.StatusOK, + expectedBody: v0.HealthResponse{ + Status: "ok", + GitHubClientID: "test-github-client-id", + }, + }, + { + name: "works with empty github client id (memory)", + config: &config.Config{ + GithubClientID: "", + }, + dbType: "memory", + expectedStatus: http.StatusOK, + expectedBody: v0.HealthResponse{ + Status: "ok", + GitHubClientID: "", + }, + }, + { + name: "works with empty github client id (mongo)", config: &config.Config{ GithubClientID: "", }, + dbType: "mongo", expectedStatus: http.StatusOK, expectedBody: v0.HealthResponse{ Status: "ok", @@ -46,8 +111,8 @@ func TestHealthHandler(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - // Create handler with the test config - handler := v0.HealthHandler(tc.config) + // Create handler with the test config and mock database + handler := v0.HealthHandler(tc.config, &mockDatabase{dbType: tc.dbType}) // Create request req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/health", nil) @@ -73,19 +138,20 @@ func TestHealthHandler(t *testing.T) { assert.NoError(t, err) // Check the response body - assert.Equal(t, tc.expectedBody, resp) + assert.Equal(t, tc.expectedBody.Status, resp.Status) + assert.Equal(t, tc.expectedBody.GitHubClientID, resp.GitHubClientID) }) } } -// TestHealthHandlerIntegration tests the handler with actual HTTP requests +// Integration test using memory database type func TestHealthHandlerIntegration(t *testing.T) { // Create test server cfg := &config.Config{ GithubClientID: "integration-test-client-id", } - server := httptest.NewServer(v0.HealthHandler(cfg)) + server := httptest.NewServer(v0.HealthHandler(cfg, &mockDatabase{dbType: "memory"})) defer server.Close() // Send request to the test server @@ -118,5 +184,6 @@ func TestHealthHandlerIntegration(t *testing.T) { Status: "ok", GitHubClientID: "integration-test-client-id", } - assert.Equal(t, expectedResp, healthResp) + assert.Equal(t, expectedResp.Status, healthResp.Status) + assert.Equal(t, expectedResp.GitHubClientID, healthResp.GitHubClientID) } diff --git a/internal/api/router/router.go b/internal/api/router/router.go index 9f17880c..96eebc0c 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -6,15 +6,16 @@ import ( "github.com/modelcontextprotocol/registry/internal/auth" "github.com/modelcontextprotocol/registry/internal/config" + "github.com/modelcontextprotocol/registry/internal/database" "github.com/modelcontextprotocol/registry/internal/service" ) // New creates a new router with all API versions registered -func New(cfg *config.Config, registry service.RegistryService, authService auth.Service) *http.ServeMux { +func New(cfg *config.Config, registry service.RegistryService, authService auth.Service, db database.Database) *http.ServeMux { mux := http.NewServeMux() // Register routes for all API versions - RegisterV0Routes(mux, cfg, registry, authService) + RegisterV0Routes(mux, cfg, registry, authService, db) return mux } diff --git a/internal/api/router/v0.go b/internal/api/router/v0.go index 6d465f99..3b105caa 100644 --- a/internal/api/router/v0.go +++ b/internal/api/router/v0.go @@ -7,15 +7,17 @@ import ( v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" "github.com/modelcontextprotocol/registry/internal/auth" "github.com/modelcontextprotocol/registry/internal/config" + "github.com/modelcontextprotocol/registry/internal/database" "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, authService auth.Service, + db database.Database, ) { // Register v0 endpoints - mux.HandleFunc("/v0/health", v0.HealthHandler(cfg)) + mux.HandleFunc("/v0/health", v0.HealthHandler(cfg, db)) mux.HandleFunc("/v0/servers", v0.ServersHandler(registry)) mux.HandleFunc("/v0/servers/{id}", v0.ServersDetailHandler(registry)) mux.HandleFunc("/v0/ping", v0.PingHandler(cfg)) diff --git a/internal/api/server.go b/internal/api/server.go index c92a1a54..9693924a 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -9,6 +9,7 @@ import ( "github.com/modelcontextprotocol/registry/internal/api/router" "github.com/modelcontextprotocol/registry/internal/auth" "github.com/modelcontextprotocol/registry/internal/config" + "github.com/modelcontextprotocol/registry/internal/database" "github.com/modelcontextprotocol/registry/internal/service" ) @@ -22,9 +23,9 @@ type Server struct { } // NewServer creates a new HTTP server -func NewServer(cfg *config.Config, registryService service.RegistryService, authService auth.Service) *Server { +func NewServer(cfg *config.Config, registryService service.RegistryService, authService auth.Service, db database.Database) *Server { // Create router with all API versions registered - mux := router.New(cfg, registryService, authService) + mux := router.New(cfg, registryService, authService, db) server := &Server{ config: cfg, diff --git a/internal/appinfo/appinfo.go b/internal/appinfo/appinfo.go new file mode 100644 index 00000000..2852daa1 --- /dev/null +++ b/internal/appinfo/appinfo.go @@ -0,0 +1,22 @@ +package appinfo + +import "time" + +// Application start time +var startTime = time.Now() +var Version = "v0.1.0" + +// Initialize sets the application start time +func Initialize() { + startTime = time.Now() +} + +// GetUptime returns the time the application has been running +func GetUptime() time.Duration { + return time.Since(startTime).Truncate(time.Second) +} + +// GetUptimeString returns a formatted uptime string, e.g. "2h30m15s" +func GetUptimeString() string { + return GetUptime().String() +} diff --git a/internal/database/database.go b/internal/database/database.go index d145f0c6..938d10be 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -26,6 +26,8 @@ type Database interface { Publish(ctx context.Context, serverDetail *model.ServerDetail) error // ImportSeed imports initial data from a seed file ImportSeed(ctx context.Context, seedFilePath string) error + // Connection returns information about the database connection + Connection() *ConnectionInfo // Close closes the database connection Close() error } @@ -46,6 +48,8 @@ type ConnectionInfo struct { Type ConnectionType // IsConnected indicates whether the database is currently connected IsConnected bool + // CollectionCount indicates the number of collections in the database + CollectionCount int // 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 diff --git a/internal/database/memory.go b/internal/database/memory.go index 6cd6cb01..fdcb8cb6 100644 --- a/internal/database/memory.go +++ b/internal/database/memory.go @@ -300,9 +300,15 @@ func (db *MemoryDB) Close() error { // Connection returns information about the database connection func (db *MemoryDB) Connection() *ConnectionInfo { + // Use read lock to protect concurrent access to the map + db.mu.RLock() + collectionCount := len(db.entries) + db.mu.RUnlock() + return &ConnectionInfo{ - Type: ConnectionTypeMemory, - IsConnected: true, // Memory DB is always connected - Raw: db.entries, + Type: ConnectionTypeMemory, + IsConnected: true, // Memory DB is always connected + CollectionCount: collectionCount, + Raw: db.entries, } } diff --git a/internal/database/mongo.go b/internal/database/mongo.go index 21538493..28f02ee8 100644 --- a/internal/database/mongo.go +++ b/internal/database/mongo.go @@ -292,6 +292,7 @@ func (db *MongoDB) Close() error { // Connection returns information about the database connection func (db *MongoDB) Connection() *ConnectionInfo { isConnected := false + collectionCount := 0 // Check if the client is connected if db.client != nil { // A quick ping with 1 second timeout to verify connection @@ -299,11 +300,20 @@ func (db *MongoDB) Connection() *ConnectionInfo { defer cancel() err := db.client.Ping(ctx, nil) isConnected = (err == nil) + + if isConnected { + names, err := db.client.ListDatabaseNames(context.Background(), bson.M{}) + if err != nil { + log.Printf("Error listing database names: %v", err) + } + collectionCount = len(names) + } } return &ConnectionInfo{ - Type: ConnectionTypeMongoDB, - IsConnected: isConnected, - Raw: db.client, + Type: ConnectionTypeMongoDB, + IsConnected: isConnected, + CollectionCount: collectionCount, + Raw: db.client, } }