diff --git a/cmd/registry/main.go b/cmd/registry/main.go index 3e5de1e..3eee551 100644 --- a/cmd/registry/main.go +++ b/cmd/registry/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/modelcontextprotocol/registry/internal/api" + 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/importer" @@ -39,7 +40,7 @@ func main() { // Show version information if requested if *showVersion { - log.Printf("MCP Registry v%s\n", Version) + log.Printf("MCP Registry %s\n", Version) log.Printf("Git commit: %s\n", GitCommit) log.Printf("Build time: %s\n", BuildTime) return @@ -102,8 +103,15 @@ func main() { } }() + // Prepare version information + versionInfo := &v0.VersionBody{ + Version: Version, + GitCommit: GitCommit, + BuildTime: BuildTime, + } + // Initialize HTTP server - server := api.NewServer(cfg, registryService, metrics) + server := api.NewServer(cfg, registryService, metrics, versionInfo) // Start server in a goroutine so it doesn't block signal handling go func() { diff --git a/internal/api/handlers/v0/version.go b/internal/api/handlers/v0/version.go new file mode 100644 index 0000000..a690b04 --- /dev/null +++ b/internal/api/handlers/v0/version.go @@ -0,0 +1,32 @@ +package v0 + +import ( + "context" + "net/http" + "strings" + + "github.com/danielgtaylor/huma/v2" +) + +// VersionBody represents the version information +type VersionBody struct { + Version string `json:"version" example:"v1.0.0" doc:"Application version"` + GitCommit string `json:"git_commit" example:"abc123d" doc:"Git commit SHA"` + BuildTime string `json:"build_time" example:"2025-10-14T12:00:00Z" doc:"Build timestamp"` +} + +// RegisterVersionEndpoint registers the version endpoint with a custom path prefix +func RegisterVersionEndpoint(api huma.API, pathPrefix string, versionInfo *VersionBody) { + huma.Register(api, huma.Operation{ + OperationID: "get-version" + strings.ReplaceAll(pathPrefix, "/", "-"), + Method: http.MethodGet, + Path: pathPrefix + "/version", + Summary: "Get version information", + Description: "Returns the version, git commit, and build time of the registry application", + Tags: []string{"version"}, + }, func(_ context.Context, _ *struct{}) (*Response[VersionBody], error) { + return &Response[VersionBody]{ + Body: *versionInfo, + }, nil + }) +} diff --git a/internal/api/handlers/v0/version_test.go b/internal/api/handlers/v0/version_test.go new file mode 100644 index 0000000..0d729a3 --- /dev/null +++ b/internal/api/handlers/v0/version_test.go @@ -0,0 +1,76 @@ +package v0_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humago" + "github.com/stretchr/testify/assert" + + v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" +) + +func TestVersionEndpoint(t *testing.T) { + // Test cases + testCases := []struct { + name string + versionInfo *v0.VersionBody + expectedBody map[string]string + }{ + { + name: "returns version information", + versionInfo: &v0.VersionBody{ + Version: "v1.2.3", + GitCommit: "abc123def456", + BuildTime: "2025-10-14T12:00:00Z", + }, + expectedBody: map[string]string{ + "version": "v1.2.3", + "git_commit": "abc123def456", + "build_time": "2025-10-14T12:00:00Z", + }, + }, + { + name: "returns dev version information", + versionInfo: &v0.VersionBody{ + Version: "dev", + GitCommit: "unknown", + BuildTime: "unknown", + }, + expectedBody: map[string]string{ + "version": "dev", + "git_commit": "unknown", + "build_time": "unknown", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a new test API + mux := http.NewServeMux() + api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) + + // Register the version endpoint + v0.RegisterVersionEndpoint(api, "/v0", tc.versionInfo) + + // Create a test request + req := httptest.NewRequest(http.MethodGet, "/v0/version", nil) + w := httptest.NewRecorder() + + // Serve the request + mux.ServeHTTP(w, req) + + // Check the status code + assert.Equal(t, http.StatusOK, w.Code) + + // Check the response body + body := w.Body.String() + assert.Contains(t, body, `"version":"`+tc.expectedBody["version"]+`"`) + assert.Contains(t, body, `"git_commit":"`+tc.expectedBody["git_commit"]+`"`) + assert.Contains(t, body, `"build_time":"`+tc.expectedBody["build_time"]+`"`) + }) + } +} diff --git a/internal/api/openapi_compliance_test.go b/internal/api/openapi_compliance_test.go index 5c4e57b..4877274 100644 --- a/internal/api/openapi_compliance_test.go +++ b/internal/api/openapi_compliance_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" "github.com/modelcontextprotocol/registry/internal/api/router" "github.com/modelcontextprotocol/registry/internal/config" ) @@ -42,8 +43,15 @@ func TestOpenAPIEndpointCompliance(t *testing.T) { JWTPrivateKey: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", // 32-byte hex key } + // Create version info for testing + versionInfo := &v0.VersionBody{ + Version: "test", + GitCommit: "test", + BuildTime: "test", + } + // Register V0 routes exactly like production does - router.RegisterV0Routes(api, cfg, nil, nil) // nil service and metrics for schema testing + router.RegisterV0Routes(api, cfg, nil, nil, versionInfo) // nil service and metrics for schema testing // Get the OpenAPI schema req := httptest.NewRequest(http.MethodGet, "/openapi.yaml", nil) diff --git a/internal/api/router/router.go b/internal/api/router/router.go index 1ce02e2..73283a0 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -13,6 +13,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" + v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" "github.com/modelcontextprotocol/registry/internal/config" "github.com/modelcontextprotocol/registry/internal/service" "github.com/modelcontextprotocol/registry/internal/telemetry" @@ -129,7 +130,7 @@ func handle404(w http.ResponseWriter, r *http.Request) { } // NewHumaAPI creates a new Huma API with all routes registered -func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http.ServeMux, metrics *telemetry.Metrics) huma.API { +func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http.ServeMux, metrics *telemetry.Metrics, versionInfo *v0.VersionBody) huma.API { // Create Huma API configuration humaConfig := huma.DefaultConfig("Official MCP Registry", "1.0.0") humaConfig.Info.Description = "A community driven registry service for Model Context Protocol (MCP) servers.\n\n[GitHub repository](https://github.com/modelcontextprotocol/registry) | [Documentation](https://github.com/modelcontextprotocol/registry/tree/main/docs)" @@ -165,6 +166,10 @@ func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http. Name: "ping", Description: "Simple ping endpoint for testing connectivity", }, + { + Name: "version", + Description: "Version information endpoint for retrieving build and version details", + }, } // Add metrics middleware with options @@ -173,8 +178,8 @@ func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http. )) // Register routes for all API versions - RegisterV0Routes(api, cfg, registry, metrics) - RegisterV0_1Routes(api, cfg, registry, metrics) + RegisterV0Routes(api, cfg, registry, metrics, versionInfo) + RegisterV0_1Routes(api, cfg, registry, metrics, versionInfo) // Add /metrics for Prometheus metrics using promhttp mux.Handle("/metrics", metrics.PrometheusHandler()) diff --git a/internal/api/router/v0.go b/internal/api/router/v0.go index 45e0210..eae032d 100644 --- a/internal/api/router/v0.go +++ b/internal/api/router/v0.go @@ -12,10 +12,11 @@ import ( ) func RegisterV0Routes( - api huma.API, cfg *config.Config, registry service.RegistryService, metrics *telemetry.Metrics, + api huma.API, cfg *config.Config, registry service.RegistryService, metrics *telemetry.Metrics, versionInfo *v0.VersionBody, ) { v0.RegisterHealthEndpoint(api, "/v0", cfg, metrics) v0.RegisterPingEndpoint(api, "/v0") + v0.RegisterVersionEndpoint(api, "/v0", versionInfo) v0.RegisterServersEndpoints(api, "/v0", registry) v0.RegisterEditEndpoints(api, "/v0", registry, cfg) v0auth.RegisterAuthEndpoints(api, "/v0", cfg) @@ -23,10 +24,11 @@ func RegisterV0Routes( } func RegisterV0_1Routes( - api huma.API, cfg *config.Config, registry service.RegistryService, metrics *telemetry.Metrics, + api huma.API, cfg *config.Config, registry service.RegistryService, metrics *telemetry.Metrics, versionInfo *v0.VersionBody, ) { v0.RegisterHealthEndpoint(api, "/v0.1", cfg, metrics) v0.RegisterPingEndpoint(api, "/v0.1") + v0.RegisterVersionEndpoint(api, "/v0.1", versionInfo) v0.RegisterServersEndpoints(api, "/v0.1", registry) v0.RegisterEditEndpoints(api, "/v0.1", registry, cfg) v0auth.RegisterAuthEndpoints(api, "/v0.1", cfg) diff --git a/internal/api/server.go b/internal/api/server.go index ef3c27f..dac60da 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -9,6 +9,7 @@ import ( "github.com/danielgtaylor/huma/v2" + v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" "github.com/modelcontextprotocol/registry/internal/api/router" "github.com/modelcontextprotocol/registry/internal/config" "github.com/modelcontextprotocol/registry/internal/service" @@ -42,11 +43,11 @@ type Server struct { } // NewServer creates a new HTTP server -func NewServer(cfg *config.Config, registryService service.RegistryService, metrics *telemetry.Metrics) *Server { +func NewServer(cfg *config.Config, registryService service.RegistryService, metrics *telemetry.Metrics, versionInfo *v0.VersionBody) *Server { // Create HTTP mux and Huma API mux := http.NewServeMux() - api := router.NewHumaAPI(cfg, registryService, mux, metrics) + api := router.NewHumaAPI(cfg, registryService, mux, metrics, versionInfo) // Wrap the mux with trailing slash middleware handler := TrailingSlashMiddleware(mux)