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