From b21c6f564d9197f14006a87928181539f57b28ab Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Mon, 29 Sep 2025 23:30:35 +0300 Subject: [PATCH] Changes on the server.json format and the API (#562) --- Makefile | 5 +- cmd/publisher/commands/init.go | 3 +- cmd/publisher/commands/publish.go | 13 +- cmd/publisher/commands/publish_test.go | 5 +- cmd/registry/main.go | 4 +- data/seed.json | 4312 ++--------------- .../guides/administration/admin-operations.md | 94 +- docs/guides/consuming/use-rest-api.md | 34 +- docs/guides/publishing/publish-server.md | 16 +- docs/reference/api/CHANGELOG.md | 24 + docs/reference/api/generic-registry-api.md | 34 +- docs/reference/api/official-registry-api.md | 2 +- docs/reference/api/openapi.yaml | 181 +- docs/reference/server-json/CHANGELOG.md | 20 +- .../server-json/generic-server-json.md | 14 +- docs/reference/server-json/server.schema.json | 15 +- internal/api/handlers/v0/auth/none.go | 12 +- internal/api/handlers/v0/auth/none_test.go | 10 +- internal/api/handlers/v0/edit.go | 64 +- internal/api/handlers/v0/edit_test.go | 767 ++- internal/api/handlers/v0/publish.go | 8 +- .../handlers/v0/publish_integration_test.go | 24 +- .../v0/publish_registry_validation_test.go | 10 +- internal/api/handlers/v0/publish_test.go | 2 +- internal/api/handlers/v0/servers.go | 127 +- internal/api/handlers/v0/servers_test.go | 1083 ++--- internal/api/handlers/v0/telemetry_test.go | 9 +- internal/database/database.go | 34 +- .../008_separate_official_metadata.sql | 143 + internal/database/postgres.go | 461 +- internal/database/postgres_test.go | 969 ++++ internal/importer/importer.go | 60 +- internal/importer/importer_test.go | 284 +- internal/service/registry_service.go | 364 +- internal/service/registry_service_test.go | 534 +- internal/service/service.go | 28 +- internal/validators/validators.go | 15 +- internal/validators/validators_test.go | 3 +- pkg/api/v0/types.go | 42 +- scripts/test_endpoints.sh | 34 +- scripts/test_publish.sh | 24 +- tests/integration/README.md | 2 + .../docker-compose.integration-test.yml | 2 + tests/integration/main.go | 33 +- tests/integration/run.sh | 21 +- tools/admin/takedown.sh | 29 +- 46 files changed, 4211 insertions(+), 5763 deletions(-) create mode 100644 internal/database/migrations/008_separate_official_metadata.sql create mode 100644 internal/database/postgres_test.go diff --git a/Makefile b/Makefile index 1ab5e8e..31ed260 100644 --- a/Makefile +++ b/Makefile @@ -59,13 +59,16 @@ lint-fix: ## Run linter with auto-fix (includes formatting) golangci-lint run --fix --timeout=5m # Combined targets -check: lint validate test-all ## Run all checks (lint, validate, unit tests) +check: dev-down lint validate test-all ## Run all checks (lint, validate, unit tests) and ensure dev environment is down @echo "All checks passed!" # Development targets dev-compose: ## Start development environment with Docker Compose (builds image automatically) docker compose up --build +dev-down: ## Stop development environment + docker compose down + # Cleanup clean: ## Clean build artifacts and coverage files rm -rf bin diff --git a/cmd/publisher/commands/init.go b/cmd/publisher/commands/init.go index 7b2b32e..5ca2b4b 100644 --- a/cmd/publisher/commands/init.go +++ b/cmd/publisher/commands/init.go @@ -301,10 +301,9 @@ func createServerJSON( // Create server structure return apiv0.ServerJSON{ - Schema: "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + Schema: "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", Name: name, Description: description, - Status: model.StatusActive, Repository: model.Repository{ URL: repoURL, Source: repoSource, diff --git a/cmd/publisher/commands/publish.go b/cmd/publisher/commands/publish.go index a72671c..0f6b959 100644 --- a/cmd/publisher/commands/publish.go +++ b/cmd/publisher/commands/publish.go @@ -39,7 +39,7 @@ func PublishCommand(args []string) error { // Check for deprecated schema and recommend migration // Allow empty schema (will use default) but reject old schemas - if serverJSON.Schema != "" && !strings.Contains(serverJSON.Schema, "2025-09-16") { + if serverJSON.Schema != "" && !strings.Contains(serverJSON.Schema, "2025-09-29") { return fmt.Errorf(`deprecated schema detected :%s. Migrate to the current schema format for new servers. @@ -82,14 +82,12 @@ Migrate to the current schema format for new servers. } _, _ = fmt.Fprintln(os.Stdout, "✓ Successfully published") - if serverID := response.GetServerID(); serverID != "" { - _, _ = fmt.Fprintf(os.Stdout, "✓ Server Id %s version %s\n", serverID, response.Version) - } + _, _ = fmt.Fprintf(os.Stdout, "✓ Server %s version %s\n", response.Server.Name, response.Server.Version) return nil } -func publishToRegistry(registryURL string, serverData []byte, token string) (*apiv0.ServerJSON, error) { +func publishToRegistry(registryURL string, serverData []byte, token string) (*apiv0.ServerResponse, error) { // Parse the server JSON data var serverJSON apiv0.ServerJSON err := json.Unmarshal(serverData, &serverJSON) @@ -134,9 +132,10 @@ func publishToRegistry(registryURL string, serverData []byte, token string) (*ap return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, body) } - if err := json.Unmarshal(body, &serverJSON); err != nil { + var serverResponse apiv0.ServerResponse + if err := json.Unmarshal(body, &serverResponse); err != nil { return nil, err } - return &serverJSON, nil + return &serverResponse, nil } diff --git a/cmd/publisher/commands/publish_test.go b/cmd/publisher/commands/publish_test.go index 0dd3f62..67a7022 100644 --- a/cmd/publisher/commands/publish_test.go +++ b/cmd/publisher/commands/publish_test.go @@ -44,8 +44,8 @@ func TestPublishCommand_DeprecatedSchema(t *testing.T) { errorSubstr: "deprecated schema detected", }, { - name: "current 2025-09-16 schema should pass validation", - schema: "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + name: "current 2025-09-29 schema should pass validation", + schema: "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", expectError: false, }, { @@ -69,7 +69,6 @@ func TestPublishCommand_DeprecatedSchema(t *testing.T) { Name: "com.example/test-server", Description: "A test server", Version: "1.0.0", - Status: model.StatusActive, Repository: model.Repository{ URL: "https://github.com/example/test", Source: "github", diff --git a/cmd/registry/main.go b/cmd/registry/main.go index cda8ace..3e5de1e 100644 --- a/cmd/registry/main.go +++ b/cmd/registry/main.go @@ -84,11 +84,9 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - importerService := importer.NewService(db) + importerService := importer.NewService(registryService) if err := importerService.ImportFromPath(ctx, cfg.SeedFrom); err != nil { log.Printf("Failed to import seed data: %v", err) - } else { - log.Println("Data import completed successfully") } } diff --git a/data/seed.json b/data/seed.json index ed42b04..7a4cafa 100644 --- a/data/seed.json +++ b/data/seed.json @@ -2,7 +2,6 @@ { "name": "io.github.21st-dev/magic-mcp", "description": "It's like v0 but in your Cursor/WindSurf/Cline. 21st dev Magic MCP server for working with your frontend like Magic", - "status": "active", "repository": { "url": "https://github.com/21st-dev/magic-mcp", "source": "github", @@ -25,21 +24,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4e9cf4cf-71f6-4aca-bae8-2d10a29ca2e0", - "versionId": "4e9cf4cf-71f6-4aca-bae8-2d10a29ca2e0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z" - } - } + ] }, { "name": "io.github.adfin-engineering/mcp-server-adfin", "description": "A Model Context Protocol Server for connecting with Adfin APIs ", - "status": "active", "repository": { "url": "https://github.com/Adfin-Engineering/mcp-server-adfin", "source": "github", @@ -87,21 +76,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d3669201-252f-403c-944b-c3ec0845782b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d3669201-252f-403c-944b-c3ec0845782b" - } - } + ] }, { "name": "io.github.tinyfish-io/agentql-mcp", "description": "Model Context Protocol server that integrates AgentQL's data extraction capabilities.", - "status": "active", "repository": { "url": "https://github.com/tinyfish-io/agentql-mcp", "source": "github", @@ -124,21 +103,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d86f2098-0be9-4b52-b32e-9d0f17333197", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d86f2098-0be9-4b52-b32e-9d0f17333197" - } - } + ] }, { "name": "io.github.agentrpc/agentrpc", "description": "A universal RPC layer for AI agents. Connect to any function, any language, any framework, in minutes.", - "status": "active", "repository": { "url": "https://github.com/agentrpc/agentrpc", "source": "github", @@ -172,21 +141,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c296f683-5578-474a-bbef-00cfc2716fbc", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c296f683-5578-474a-bbef-00cfc2716fbc" - } - } + ] }, { "name": "io.github.aliyun/alibabacloud-adb-mysql-mcp-server", "description": "AnalyticDB for MySQL MCP Server", - "status": "active", "repository": { "url": "https://github.com/aliyun/alibabacloud-adb-mysql-mcp-server", "source": "github", @@ -246,21 +205,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7f6e6610-4df3-490e-95e3-03ec954eb2ab", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7f6e6610-4df3-490e-95e3-03ec954eb2ab" - } - } + ] }, { "name": "io.github.aliyun/alibaba-cloud-ops-mcp-server", "description": "AlibabaCloud CloudOps MCP Server", - "status": "active", "repository": { "url": "https://github.com/aliyun/alibaba-cloud-ops-mcp-server", "source": "github", @@ -287,21 +236,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "24303d73-1ee0-4119-b2f1-3a57c17de967", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "24303d73-1ee0-4119-b2f1-3a57c17de967" - } - } + ] }, { "name": "io.github.apache/iotdb-mcp-server", "description": "Apache IoTDB MCP Server", - "status": "active", "repository": { "url": "https://github.com/apache/iotdb-mcp-server", "source": "github", @@ -365,21 +304,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "31d9b130-4676-45d9-930f-a8bc2d5a3b9e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "31d9b130-4676-45d9-930f-a8bc2d5a3b9e" - } - } + ] }, { "name": "io.github.apache/iotdb", "description": "Apache IoTDB", - "status": "active", "repository": { "url": "https://github.com/apache/iotdb", "source": "github", @@ -396,21 +325,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "330047d1-f8c2-4931-9347-d133f1717de3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "330047d1-f8c2-4931-9347-d133f1717de3" - } - } + ] }, { "name": "io.github.apify/actors-mcp-server", "description": "Model Context Protocol (MCP) Server for Apify's Actors", - "status": "active", "repository": { "url": "https://github.com/apify/actors-mcp-server", "source": "github", @@ -445,21 +364,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "195dfdbd-bb15-4752-80b6-abc9212cd80f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "195dfdbd-bb15-4752-80b6-abc9212cd80f" - } - } + ] }, { "name": "io.github.apimatic/apimatic-validator-mcp", "description": "APIMatic Validator MCP Server for validating OpenAPI specs via APIMatic's API with MCP", - "status": "active", "repository": { "url": "https://github.com/apimatic/apimatic-validator-mcp", "source": "github", @@ -482,21 +391,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5c95a170-d394-4b21-a100-bb953b872922", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5c95a170-d394-4b21-a100-bb953b872922" - } - } + ] }, { "name": "io.github.arize-ai/phoenix", "description": "AI Observability & Evaluation", - "status": "active", "repository": { "url": "https://github.com/Arize-ai/phoenix", "source": "github", @@ -513,21 +412,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "de2d7f51-ca20-4fac-abdb-85a912d26375", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "de2d7f51-ca20-4fac-abdb-85a912d26375" - } - } + ] }, { "name": "io.github.datastax/astra-db-mcp", "description": "An MCP server for Astra DB workloads", - "status": "active", "repository": { "url": "https://github.com/datastax/astra-db-mcp", "source": "github", @@ -544,21 +433,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "50f15d97-7d84-49f7-9232-f797e006358d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "50f15d97-7d84-49f7-9232-f797e006358d" - } - } + ] }, { "name": "io.github.audienseco/mcp-audiense-insights", "description": "Audiense Insights MCP Server is a server based on the Model Context Protocol (MCP) that allows Claude and other MCP-compatible clients to interact with your Audiense Insights account", - "status": "active", "repository": { "url": "https://github.com/AudienseCo/mcp-audiense-insights", "source": "github", @@ -589,21 +468,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d85fa87b-48e1-4b81-8408-9d0c446f7de3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d85fa87b-48e1-4b81-8408-9d0c446f7de3" - } - } + ] }, { "name": "io.github.bankless/onchain-mcp", "description": "Bringing the bankless onchain API to MCP", - "status": "active", "repository": { "url": "https://github.com/Bankless/onchain-mcp", "source": "github", @@ -626,21 +495,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2535daf3-637b-40d1-a4c2-41ef60a726b8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2535daf3-637b-40d1-a4c2-41ef60a726b8" - } - } + ] }, { "name": "io.github.ahnlabio/bicscan-mcp", "description": "BICScan MCP Server", - "status": "active", "repository": { "url": "https://github.com/ahnlabio/bicscan-mcp", "source": "github", @@ -684,21 +543,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "36e14e79-6228-442f-b3c7-c7080b6282f0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "36e14e79-6228-442f-b3c7-c7080b6282f0" - } - } + ] }, { "name": "io.github.bitrise-io/bitrise-mcp", "description": "MCP Server for the Bitrise API, enabling app management, build operations, artifact management and more.", - "status": "active", "repository": { "url": "https://github.com/bitrise-io/bitrise-mcp", "source": "github", @@ -752,21 +601,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9e07df79-22e2-464d-a842-06f08b875196", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9e07df79-22e2-464d-a842-06f08b875196" - } - } + ] }, { "name": "io.github.box-community/mcp-server-box", "description": "An MCP server capable of interacting with the Box API", - "status": "active", "repository": { "url": "https://github.com/box-community/mcp-server-box", "source": "github", @@ -793,21 +632,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "46bcb36f-ede9-412e-8181-7c7ece41fa04", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "46bcb36f-ede9-412e-8181-7c7ece41fa04" - } - } + ] }, { "name": "io.github.chroma-core/chroma-mcp", "description": "A Model Context Protocol (MCP) server implementation that provides database capabilities for Chroma", - "status": "active", "repository": { "url": "https://github.com/chroma-core/chroma-mcp", "source": "github", @@ -824,21 +653,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "97de67c4-a78f-4ea7-9233-4da9fe6752cf", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "97de67c4-a78f-4ea7-9233-4da9fe6752cf" - } - } + ] }, { "name": "io.github.circleci-public/mcp-server-circleci", "description": "A specialized server implementation for the Model Context Protocol (MCP) designed to integrate with CircleCI's development workflow. This project serves as a bridge between CircleCI's infrastructure and the Model Context Protocol, enabling enhanced AI-powered development experiences.", - "status": "active", "repository": { "url": "https://github.com/CircleCI-Public/mcp-server-circleci", "source": "github", @@ -865,21 +684,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a48ff624-6d95-4b8e-a0ed-498c4d838411", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a48ff624-6d95-4b8e-a0ed-498c4d838411" - } - } + ] }, { "name": "io.github.clickhouse/mcp-clickhouse", "description": "", - "status": "active", "repository": { "url": "https://github.com/ClickHouse/mcp-clickhouse", "source": "github", @@ -945,21 +754,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "98259539-3c6f-4462-b2b6-9a2567ff0545", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "98259539-3c6f-4462-b2b6-9a2567ff0545" - } - } + ] }, { "name": "com.cloudflare/mcp-server-cloudflare", "description": "", - "status": "active", "repository": { "url": "https://github.com/cloudflare/mcp-server-cloudflare", "source": "github", @@ -997,21 +796,11 @@ "type": "sse", "url": "https://bindings.mcp.cloudflare.com/sse" } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2ad7c503-7daa-46c8-9295-8a1ed53271a6", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2ad7c503-7daa-46c8-9295-8a1ed53271a6" - } - } + ] }, { "name": "io.github.codacy/codacy-mcp-server", "description": "Codacy's MCP Server implementation", - "status": "active", "repository": { "url": "https://github.com/codacy/codacy-mcp-server", "source": "github", @@ -1038,21 +827,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c746fe90-e3f6-4328-82c1-aeef49a2b6d9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c746fe90-e3f6-4328-82c1-aeef49a2b6d9" - } - } + ] }, { "name": "io.github.codelogicincengineering/codelogic-mcp-server", "description": "An MCP Server to utilize Codelogic's rich software dependency data in your AI programming assistant.", - "status": "active", "repository": { "url": "https://github.com/CodeLogicIncEngineering/codelogic-mcp-server", "source": "github", @@ -1069,21 +848,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6e6e3083-47bf-47da-9d0f-14805de093a9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6e6e3083-47bf-47da-9d0f-14805de093a9" - } - } + ] }, { "name": "io.github.comet-ml/opik-mcp", "description": "Model Context Protocol (MCP) implementation for Opik enabling seamless IDE integration and unified access to prompts, projects, traces, and metrics. ", - "status": "active", "repository": { "url": "https://github.com/comet-ml/opik-mcp", "source": "github", @@ -1156,21 +925,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "61b3cbd8-8eee-4bef-a0ef-5fcaf6449201", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "61b3cbd8-8eee-4bef-a0ef-5fcaf6449201" - } - } + ] }, { "name": "io.github.couchbase-ecosystem/mcp-server-couchbase", "description": "MCP Server to interact with data in Couchbase Clusters", - "status": "active", "repository": { "url": "https://github.com/Couchbase-Ecosystem/mcp-server-couchbase", "source": "github", @@ -1226,21 +985,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3f49614c-5424-4875-92ef-9697923375cf", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3f49614c-5424-4875-92ef-9697923375cf" - } - } + ] }, { "name": "io.github.its-dart/dart-mcp-server", "description": "Dart AI Model Context Protocol (MCP) server", - "status": "active", "repository": { "url": "https://github.com/its-dart/dart-mcp-server", "source": "github", @@ -1302,21 +1051,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9c2d91f0-76c4-4292-a19f-e81e962954b5", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9c2d91f0-76c4-4292-a19f-e81e962954b5" - } - } + ] }, { "name": "io.github.devhub/devhub-cms-mcp", "description": "DevHub CMS LLM integration through the Model Context Protocol", - "status": "active", "repository": { "url": "https://github.com/devhub/devhub-cms-mcp", "source": "github", @@ -1333,21 +1072,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1743c861-7e1a-42b2-a300-6059fcd82cb9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1743c861-7e1a-42b2-a300-6059fcd82cb9" - } - } + ] }, { "name": "io.github.dynatrace-oss/dynatrace-mcp", "description": "MCP server for Dynatrace Observability", - "status": "active", "repository": { "url": "https://github.com/dynatrace-oss/dynatrace-mcp", "source": "github", @@ -1364,21 +1093,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3afff2d4-d77f-42c2-bf39-ce786ecb0ecf", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3afff2d4-d77f-42c2-bf39-ce786ecb0ecf" - } - } + ] }, { "name": "io.github.edgee-cloud/mcp-server-edgee", "description": "", - "status": "active", "repository": { "url": "https://github.com/edgee-cloud/mcp-server-edgee", "source": "github", @@ -1401,21 +1120,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7c870985-c457-4221-8088-977680764f70", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7c870985-c457-4221-8088-977680764f70" - } - } + ] }, { "name": "io.github.edubase/mcp", "description": "The EduBase MCP server enables Claude and other LLMs to interact with EduBase's comprehensive e-learning platform through the Model Context Protocol (MCP).", - "status": "active", "repository": { "url": "https://github.com/EduBase/MCP", "source": "github", @@ -1505,21 +1214,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "33102362-9e44-442f-8aa9-4443ce50c396", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "33102362-9e44-442f-8aa9-4443ce50c396" - } - } + ] }, { "name": "io.github.elastic/mcp-server-elasticsearch", "description": "", - "status": "active", "repository": { "url": "https://github.com/elastic/mcp-server-elasticsearch", "source": "github", @@ -1557,21 +1256,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1b8a16d9-0648-4790-b853-d919a6be16e9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1b8a16d9-0648-4790-b853-d919a6be16e9" - } - } + ] }, { "name": "io.github.esignaturescom/mcp-server-esignatures", "description": "MCP server for eSignatures (https://esignatures.com)", - "status": "active", "repository": { "url": "https://github.com/esignaturescom/mcp-server-esignatures", "source": "github", @@ -1588,21 +1277,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d689b152-64e8-4f5d-a937-91331fa3a771", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d689b152-64e8-4f5d-a937-91331fa3a771" - } - } + ] }, { "name": "io.github.exa-labs/exa-mcp-server", "description": "Claude can perform Web Search | Exa with MCP (Model Context Protocol)", - "status": "active", "repository": { "url": "https://github.com/exa-labs/exa-mcp-server", "source": "github", @@ -1637,21 +1316,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2a2aa283-57c8-474f-8a27-27954214cb6b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2a2aa283-57c8-474f-8a27-27954214cb6b" - } - } + ] }, { "name": "io.github.fewsats/fewsats-mcp", "description": "Fewsats MCP server", - "status": "active", "repository": { "url": "https://github.com/Fewsats/fewsats-mcp", "source": "github", @@ -1674,21 +1343,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0e7533d0-5925-4e06-a6bf-5da4e4e21aec", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0e7533d0-5925-4e06-a6bf-5da4e4e21aec" - } - } + ] }, { "name": "io.github.fibery-inc/fibery-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/Fibery-inc/fibery-mcp-server", "source": "github", @@ -1746,21 +1405,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "78c126ff-906a-4596-9bf4-db2cf61796b8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "78c126ff-906a-4596-9bf4-db2cf61796b8" - } - } + ] }, { "name": "io.github.financial-datasets/mcp-server", "description": "An MCP server for interacting with the Financial Datasets stock market API.", - "status": "active", "repository": { "url": "https://github.com/financial-datasets/mcp-server", "source": "github", @@ -1787,21 +1436,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2d870a4e-d521-4d66-a471-abce884ea07b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2d870a4e-d521-4d66-a471-abce884ea07b" - } - } + ] }, { "name": "io.github.mendableai/firecrawl-mcp-server", "description": "Official Firecrawl MCP Server - Adds powerful web scraping to Cursor, Claude and any other LLM clients.", - "status": "active", "repository": { "url": "https://github.com/mendableai/firecrawl-mcp-server", "source": "github", @@ -1818,21 +1457,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "13b91785-9041-468d-a136-e6c9e9dd7be9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "13b91785-9041-468d-a136-e6c9e9dd7be9" - } - } + ] }, { "name": "io.github.fireproof-storage/mcp-database-server", "description": "Store and load JSON documents from LLM tool use", - "status": "active", "repository": { "url": "https://github.com/fireproof-storage/mcp-database-server", "source": "github", @@ -1849,21 +1478,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0bee6074-d16a-4482-a315-5b8f0f1abcf1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0bee6074-d16a-4482-a315-5b8f0f1abcf1" - } - } + ] }, { "name": "io.github.gleanwork/mcp-server", "description": "MCP server for Glean API integration", - "status": "active", "repository": { "url": "https://github.com/gleanwork/mcp-server", "source": "github", @@ -1890,21 +1509,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3e50df21-8223-440d-bad9-dfc9ce117af4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3e50df21-8223-440d-bad9-dfc9ce117af4" - } - } + ] }, { "name": "io.github.nota/gyazo-mcp-server", "description": "Official Model Context Protocol server for Gyazo", - "status": "active", "repository": { "url": "https://github.com/nota/gyazo-mcp-server", "source": "github", @@ -1977,21 +1586,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "11e3a5f5-867c-4f1e-ba79-1b803550fb1a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "11e3a5f5-867c-4f1e-ba79-1b803550fb1a" - } - } + ] }, { "name": "io.github.gotohuman/gotohuman-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/gotohuman/gotohuman-mcp-server", "source": "github", @@ -2014,21 +1613,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0f970fc6-418a-45c7-a665-00e725e8338d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0f970fc6-418a-45c7-a665-00e725e8338d" - } - } + ] }, { "name": "io.github.grafana/mcp-grafana", "description": "MCP server for Grafana", - "status": "active", "repository": { "url": "https://github.com/grafana/mcp-grafana", "source": "github", @@ -2113,21 +1702,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1329d108-67d0-488a-85a1-cc65b58cf5cd", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1329d108-67d0-488a-85a1-cc65b58cf5cd" - } - } + ] }, { "name": "io.github.graphlit/graphlit-mcp-server", "description": "Model Context Protocol (MCP) Server for Graphlit Platform", - "status": "active", "repository": { "url": "https://github.com/graphlit/graphlit-mcp-server", "source": "github", @@ -2158,21 +1737,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e44b5cad-3801-4403-b8ee-67f3ea1cd909", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e44b5cad-3801-4403-b8ee-67f3ea1cd909" - } - } + ] }, { "name": "io.github.greptimeteam/greptimedb-mcp-server", "description": "A Model Context Protocol (MCP) server implementation for GreptimeDB", - "status": "active", "repository": { "url": "https://github.com/GreptimeTeam/greptimedb-mcp-server", "source": "github", @@ -2207,21 +1776,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7974b19d-8030-428b-a016-168c65058b41", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7974b19d-8030-428b-a016-168c65058b41" - } - } + ] }, { "name": "io.github.heroku/heroku-mcp-server", "description": "Heroku Platform MCP Server using the Heroku CLI", - "status": "active", "repository": { "url": "https://github.com/heroku/heroku-mcp-server", "source": "github", @@ -2238,21 +1797,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "cb1a844b-0145-433b-9a6f-f8ef19b2b3f4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "cb1a844b-0145-433b-9a6f-f8ef19b2b3f4" - } - } + ] }, { "name": "io.github.aliyun/alibabacloud-hologres-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/aliyun/alibabacloud-hologres-mcp-server", "source": "github", @@ -2291,21 +1840,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e67dda78-7d99-43e5-9e98-c80267f34940", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e67dda78-7d99-43e5-9e98-c80267f34940" - } - } + ] }, { "name": "io.github.hyperbrowserai/mcp", "description": "A MCP server implementation for hyperbrowser", - "status": "active", "repository": { "url": "https://github.com/hyperbrowserai/mcp", "source": "github", @@ -2328,21 +1867,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "feb2b365-5fab-461e-a8d3-89e9349ee743", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "feb2b365-5fab-461e-a8d3-89e9349ee743" - } - } + ] }, { "name": "io.github.elie222/inbox-zero", "description": "The world's best AI personal assistant for email. Open source app to help you reach inbox zero fast.", - "status": "active", "repository": { "url": "https://github.com/elie222/inbox-zero", "source": "github", @@ -2359,21 +1888,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "afd9f984-ea69-4a8c-86ac-fdb58f3ab37c", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "afd9f984-ea69-4a8c-86ac-fdb58f3ab37c" - } - } + ] }, { "name": "io.github.integration-app/mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/integration-app/mcp-server", "source": "github", @@ -2400,21 +1919,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "584b368d-b659-4bac-9036-dc319cf8d88d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "584b368d-b659-4bac-9036-dc319cf8d88d" - } - } + ] }, { "name": "io.github.jetbrains/mcp-jetbrains", "description": "A model context protocol server to work with JetBrains IDEs: IntelliJ, PyCharm, WebStorm, etc. Also, works with Android Studio", - "status": "active", "repository": { "url": "https://github.com/JetBrains/mcp-jetbrains", "source": "github", @@ -2437,21 +1946,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a51c6dec-56ed-403e-831b-fa5f155f8696", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a51c6dec-56ed-403e-831b-fa5f155f8696" - } - } + ] }, { "name": "io.github.kagisearch/kagimcp", "description": "A Model Context Protocol (MCP) server for Kagi search & other tools.", - "status": "active", "repository": { "url": "https://github.com/kagisearch/kagimcp", "source": "github", @@ -2499,21 +1998,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "77963df5-3dc8-4961-ae0c-03e448b8713c", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "77963df5-3dc8-4961-ae0c-03e448b8713c" - } - } + ] }, { "name": "io.github.keboola/keboola-mcp-server", "description": "Model Context Protocol (MCP) Server for the Keboola Platform", - "status": "active", "repository": { "url": "https://github.com/keboola/mcp-server", "source": "github", @@ -2571,21 +2060,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "eba4813b-2591-4264-b7bc-c485b7652e92", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "eba4813b-2591-4264-b7bc-c485b7652e92" - } - } + ] }, { "name": "io.github.translated/lara-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/translated/lara-mcp", "source": "github", @@ -2652,21 +2131,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5026f1b0-0426-4ea2-8ec2-fab1fabb0a7e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5026f1b0-0426-4ea2-8ec2-fab1fabb0a7e" - } - } + ] }, { "name": "io.github.pydantic/logfire-mcp", "description": "The Logfire MCP Server is here! :tada:", - "status": "active", "repository": { "url": "https://github.com/pydantic/logfire-mcp", "source": "github", @@ -2705,21 +2174,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6e66e04c-86cc-43ec-90ed-1a91a0e4eb76", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6e66e04c-86cc-43ec-90ed-1a91a0e4eb76" - } - } + ] }, { "name": "io.github.langfuse/mcp-server-langfuse", "description": "Model Context Protocol (MCP) Server for Langfuse Prompt Management. This server allows you to access and manage your Langfuse prompts through the Model Context Protocol.", - "status": "active", "repository": { "url": "https://github.com/langfuse/mcp-server-langfuse", "source": "github", @@ -2750,21 +2209,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f88b50da-f452-452b-9f22-1c7da7542b36", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f88b50da-f452-452b-9f22-1c7da7542b36" - } - } + ] }, { "name": "io.github.lingodotdev/lingo.dev", - "description": "\u26a1\ufe0f Open-source AI-powered CLI for web & mobile localization. Bring your own LLM or use Lingo.dev-managed localization engine.", - "status": "active", + "description": "⚡️ Open-source AI-powered CLI for web & mobile localization. Bring your own LLM or use Lingo.dev-managed localization engine.", "repository": { "url": "https://github.com/lingodotdev/lingo.dev", "source": "github", @@ -2781,21 +2230,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1e2d718a-95b0-460b-8e57-86feec5356ce", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1e2d718a-95b0-460b-8e57-86feec5356ce" - } - } + ] }, { "name": "io.github.integromat/make-mcp-server", "description": "Make MCP Server", - "status": "active", "repository": { "url": "https://github.com/integromat/make-mcp-server", "source": "github", @@ -2826,21 +2265,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f4fca7d7-fbab-4174-ba76-1396c3451cf1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f4fca7d7-fbab-4174-ba76-1396c3451cf1" - } - } + ] }, { "name": "io.github.meilisearch/meilisearch-mcp", "description": "A Model Context Protocol (MCP) server for interacting with Meilisearch through LLM interfaces.", - "status": "active", "repository": { "url": "https://github.com/meilisearch/meilisearch-mcp", "source": "github", @@ -2857,21 +2286,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "11d467e3-6001-4e60-ba90-494ad9eb7ad2", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "11d467e3-6001-4e60-ba90-494ad9eb7ad2" - } - } + ] }, { "name": "io.github.memgraph/mcp-memgraph", "description": "Memgraph MCP Server", - "status": "active", "repository": { "url": "https://github.com/memgraph/mcp-memgraph", "source": "github", @@ -2888,21 +2307,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "dd730dba-107c-40f0-b172-85500a22b5c2", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "dd730dba-107c-40f0-b172-85500a22b5c2" - } - } + ] }, { "name": "io.github.zilliztech/mcp-server-milvus", "description": "Model Context Protocol Servers for Milvus", - "status": "active", "repository": { "url": "https://github.com/zilliztech/mcp-server-milvus", "source": "github", @@ -2950,21 +2359,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e458676a-2727-4daf-84c5-d607932fa000", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e458676a-2727-4daf-84c5-d607932fa000" - } - } + ] }, { "name": "io.github.momentohq/mcp-momento", "description": "A model context protocol server for Momento", - "status": "active", "repository": { "url": "https://github.com/momentohq/mcp-momento", "source": "github", @@ -2995,21 +2394,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b99e5603-6346-4b2c-bae8-876c9b71e18b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b99e5603-6346-4b2c-bae8-876c9b71e18b" - } - } + ] }, { "name": "io.github.mongodb-js/mongodb-mcp-server", "description": "A Model Context Protocol server to connect to MongoDB databases and MongoDB Atlas Clusters.", - "status": "active", "repository": { "url": "https://github.com/mongodb-js/mongodb-mcp-server", "source": "github", @@ -3048,21 +2437,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9fd2c1fb-b8a6-4411-89cf-d4612cc3a539", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9fd2c1fb-b8a6-4411-89cf-d4612cc3a539" - } - } + ] }, { "name": "io.github.motherduckdb/mcp-server-motherduck", "description": "MCP server for DuckDB and MotherDuck", - "status": "active", "repository": { "url": "https://github.com/motherduckdb/mcp-server-motherduck", "source": "github", @@ -3120,21 +2499,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b3fa3fa4-901d-4584-82cf-d027dbdf290b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b3fa3fa4-901d-4584-82cf-d027dbdf290b" - } - } + ] }, { "name": "io.github.neondatabase/mcp-server-neon", "description": "MCP server for interacting with Neon Management API and databases", - "status": "active", "repository": { "url": "https://github.com/neondatabase-labs/mcp-server-neon", "source": "github", @@ -3171,21 +2540,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9a6bd437-fe20-4f40-b776-5507f2871441", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9a6bd437-fe20-4f40-b776-5507f2871441" - } - } + ] }, { "name": "io.github.oceanbase/mcp-oceanbase", "description": "MCP Server for OceanBase database and its tools", - "status": "active", "repository": { "url": "https://github.com/oceanbase/mcp-oceanbase", "source": "github", @@ -3202,21 +2561,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "afe07206-691c-4bfb-bf17-8a39014cf4a7", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "afe07206-691c-4bfb-bf17-8a39014cf4a7" - } - } + ] }, { "name": "io.github.octagonai/octagon-mcp-server", "description": "A free MCP server to analyze and extract insights from public filings, earnings transcripts, financial metrics, stock market data, private market transactions, and deep web-based research within Claude Desktop and other popular MCP clients.", - "status": "active", "repository": { "url": "https://github.com/OctagonAI/octagon-mcp-server", "source": "github", @@ -3239,21 +2588,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2d53cc39-27e0-446f-96b5-1324bab9eb21", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2d53cc39-27e0-446f-96b5-1324bab9eb21" - } - } + ] }, { "name": "io.github.oxylabs/oxylabs-mcp", "description": "Official Oxylabs MCP integration", - "status": "active", "repository": { "url": "https://github.com/oxylabs/oxylabs-mcp", "source": "github", @@ -3301,21 +2640,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4632fbcb-d313-4e26-ad1d-aea8f57524f7", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4632fbcb-d313-4e26-ad1d-aea8f57524f7" - } - } + ] }, { "name": "io.github.paddlehq/paddle-mcp-server", "description": "Interact with the Paddle API using AI assistants like Claude, or in AI-powered IDEs like Cursor. Manage product catalog, billing and subscriptions, and reports.", - "status": "active", "repository": { "url": "https://github.com/PaddleHQ/paddle-mcp-server", "source": "github", @@ -3353,21 +2682,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "be79836b-8a99-4e4e-9487-c81c02833f4a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "be79836b-8a99-4e4e-9487-c81c02833f4a" - } - } + ] }, { "name": "io.github.pinecone-io/pinecone-mcp", "description": "Connect your Pinecone projects to Cursor, Claude, and other AI assistants", - "status": "active", "repository": { "url": "https://github.com/pinecone-io/pinecone-mcp", "source": "github", @@ -3384,21 +2703,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ed4c9613-5046-4570-8d15-209f7294ba75", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ed4c9613-5046-4570-8d15-209f7294ba75" - } - } + ] }, { "name": "io.github.pinecone-io/assistant-mcp", "description": "Pinecone Assistant MCP server", - "status": "active", "repository": { "url": "https://github.com/pinecone-io/assistant-mcp", "source": "github", @@ -3474,21 +2783,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "08668c6a-c2f6-47b0-a4e0-75a8ee9bcf10", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "08668c6a-c2f6-47b0-a4e0-75a8ee9bcf10" - } - } + ] }, { "name": "io.github.ragieai/ragie-mcp-server", "description": "Ragie Model Context Protocol Server", - "status": "active", "repository": { "url": "https://github.com/ragieai/ragie-mcp-server", "source": "github", @@ -3523,21 +2822,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "324d6879-eb6f-4565-bdeb-6c4b00d91384", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "324d6879-eb6f-4565-bdeb-6c4b00d91384" - } - } + ] }, { "name": "io.github.redis/mcp-redis-cloud", "description": "", - "status": "active", "repository": { "url": "https://github.com/redis/mcp-redis-cloud", "source": "github", @@ -3596,21 +2885,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b8c0e12b-0c04-486d-95a7-caa33b9eefeb", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b8c0e12b-0c04-486d-95a7-caa33b9eefeb" - } - } + ] }, { "name": "io.github.qdrant/mcp-server-qdrant", "description": "An official Qdrant Model Context Protocol (MCP) server implementation", - "status": "active", "repository": { "url": "https://github.com/qdrant/mcp-server-qdrant", "source": "github", @@ -3710,21 +2989,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "823afe6f-4494-4fc1-a2ab-2ca989e59d21", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "823afe6f-4494-4fc1-a2ab-2ca989e59d21" - } - } + ] }, { "name": "io.github.ramp-public/ramp-mcp", "description": "ramp_mcp", - "status": "active", "repository": { "url": "https://github.com/ramp-public/ramp_mcp", "source": "github", @@ -3794,21 +3063,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "687591a4-a5da-44df-8d55-f32e19d5658a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "687591a4-a5da-44df-8d55-f32e19d5658a" - } - } + ] }, { "name": "io.github.mindscapehq/mcp-server-raygun", "description": "", - "status": "active", "repository": { "url": "https://github.com/MindscapeHQ/mcp-server-raygun", "source": "github", @@ -3831,21 +3090,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f6ee7600-c27a-4210-9a94-29a35c41c274", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f6ee7600-c27a-4210-9a94-29a35c41c274" - } - } + ] }, { "name": "io.github.rember/rember-mcp", "description": "A Model Context Protocol (MCP) server for Rember.", - "status": "active", "repository": { "url": "https://github.com/rember/rember-mcp", "source": "github", @@ -3874,21 +3123,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5e354d7f-e19e-40ba-a760-342d5c93e9aa", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5e354d7f-e19e-40ba-a760-342d5c93e9aa" - } - } + ] }, { "name": "io.github.fatwang2/search1api-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/fatwang2/search1api-mcp", "source": "github", @@ -3911,21 +3150,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "cb1647e0-2df7-4f2a-b63b-cfaea1e5e98a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "cb1647e0-2df7-4f2a-b63b-cfaea1e5e98a" - } - } + ] }, { "name": "io.github.screenshotone/mcp", "description": "A simple implementation of an MCP server for the ScreenshotOne API", - "status": "active", "repository": { "url": "https://github.com/screenshotone/mcp", "source": "github", @@ -3959,21 +3188,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "87c7dec1-1b0c-414b-9f7b-7a113e67989d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "87c7dec1-1b0c-414b-9f7b-7a113e67989d" - } - } + ] }, { "name": "io.github.semgrep/mcp", "description": "A MCP server for using Semgrep to scan code for security vulnerabilities.", - "status": "active", "repository": { "url": "https://github.com/semgrep/mcp", "source": "github", @@ -3990,21 +3209,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d012ea8c-2f32-43e4-9583-7418d750193e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d012ea8c-2f32-43e4-9583-7418d750193e" - } - } + ] }, { "name": "io.github.singlestore-labs/mcp-server-singlestore", "description": "MCP server for interacting with SingleStore Management API and services", - "status": "active", "repository": { "url": "https://github.com/singlestore-labs/mcp-server-singlestore", "source": "github", @@ -4041,21 +3250,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "901e1b1c-7414-4240-89b8-1def9aca4708", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "901e1b1c-7414-4240-89b8-1def9aca4708" - } - } + ] }, { "name": "io.github.tavily-ai/tavily-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/tavily-ai/tavily-mcp", "source": "github", @@ -4078,21 +3277,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ba8028f7-73e6-4d3d-9974-790d438a1446", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ba8028f7-73e6-4d3d-9974-790d438a1446" - } - } + ] }, { "name": "io.github.msgbyte/tianji", "description": "Tianji: Insight into everything, Website Analytics + Uptime Monitor + Server Status. not only another GA alternatives", - "status": "active", "repository": { "url": "https://github.com/msgbyte/tianji", "source": "github", @@ -4109,21 +3298,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9dd7f908-4c1b-4ba3-a33f-86be881cc8b6", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9dd7f908-4c1b-4ba3-a33f-86be881cc8b6" - } - } + ] }, { "name": "io.github.tinybirdco/mcp-tinybird", "description": "", - "status": "active", "repository": { "url": "https://github.com/tinybirdco/mcp-tinybird", "source": "github", @@ -4140,21 +3319,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "adb9a058-774f-4505-aa80-1afa9bab8cb9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "adb9a058-774f-4505-aa80-1afa9bab8cb9" - } - } + ] }, { "name": "io.github.unstructured-io/uns-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/Unstructured-IO/UNS-MCP", "source": "github", @@ -4192,21 +3361,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "8b01f838-0cb6-4c17-b381-108713321d6d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "8b01f838-0cb6-4c17-b381-108713321d6d" - } - } + ] }, { "name": "io.github.vectorize-io/vectorize-mcp-server", "description": "Official Vectorize MCP Server", - "status": "active", "repository": { "url": "https://github.com/vectorize-io/vectorize-mcp-server", "source": "github", @@ -4245,21 +3404,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c1b727f7-1570-465f-8153-585840de260a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c1b727f7-1570-465f-8153-585840de260a" - } - } + ] }, { "name": "io.github.verodat/verodat-mcp-server", "description": "Verodat MCP Server Implementation", - "status": "active", "repository": { "url": "https://github.com/Verodat/verodat-mcp-server", "source": "github", @@ -4297,21 +3446,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c0c30db1-cc08-426e-886f-0bdbcd31d101", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c0c30db1-cc08-426e-886f-0bdbcd31d101" - } - } + ] }, { "name": "io.github.veyrax/veyrax-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/VeyraX/veyrax-mcp", "source": "github", @@ -4328,21 +3467,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2cc83e0a-3ca5-4e21-b346-83254cc72e1f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2cc83e0a-3ca5-4e21-b346-83254cc72e1f" - } - } + ] }, { "name": "io.github.xeroapi/xero-mcp-server", "description": "An MCP server that integrates with the MCP protocol. https://modelcontextprotocol.io/introduction", - "status": "active", "repository": { "url": "https://github.com/XeroAPI/xero-mcp-server", "source": "github", @@ -4380,21 +3509,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4bc3b6d3-666e-4bb7-8ad7-6b38bc9c5ab8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4bc3b6d3-666e-4bb7-8ad7-6b38bc9c5ab8" - } - } + ] }, { "name": "io.github.simon-kansara/ableton-live-mcp-server", "description": "MCP Server implementation for Ableton Live OSC control", - "status": "active", "repository": { "url": "https://github.com/Simon-Kansara/ableton-live-mcp-server", "source": "github", @@ -4411,21 +3530,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "60b95692-579f-4de4-8095-1efdca5d8591", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "60b95692-579f-4de4-8095-1efdca5d8591" - } - } + ] }, { "name": "io.github.ahujasid/ableton-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/ahujasid/ableton-mcp", "source": "github", @@ -4442,21 +3551,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "98b8992b-77cc-43a5-825e-d14270fd9ca5", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "98b8992b-77cc-43a5-825e-d14270fd9ca5" - } - } + ] }, { "name": "io.github.openbnb-org/mcp-server-airbnb", "description": "", - "status": "active", "repository": { "url": "https://github.com/openbnb-org/mcp-server-airbnb", "source": "github", @@ -4484,21 +3583,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1101c394-e912-42b6-85be-d2479b72d27f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1101c394-e912-42b6-85be-d2479b72d27f" - } - } + ] }, { "name": "io.github.goplausible/algorand-mcp", "description": "Algorand Model Context Protocol (Server & Client)", - "status": "active", "repository": { "url": "https://github.com/GoPlausible/algorand-mcp", "source": "github", @@ -4515,21 +3604,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4813a35a-7d43-42fa-999d-86e68b2aca1a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4813a35a-7d43-42fa-999d-86e68b2aca1a" - } - } + ] }, { "name": "io.github.yangkyeongmo/mcp-server-apache-airflow", "description": "", - "status": "active", "repository": { "url": "https://github.com/yangkyeongmo/mcp-server-apache-airflow", "source": "github", @@ -4581,21 +3660,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ae6d4cd8-c930-4737-9aba-93609f53cde1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ae6d4cd8-c930-4737-9aba-93609f53cde1" - } - } + ] }, { "name": "io.github.domdomegg/airtable-mcp-server", - "description": "\ud83d\uddc2\ufe0f\ud83e\udd16 Airtable Model Context Protocol Server, for allowing AI systems to interact with your Airtable bases", - "status": "active", + "description": "🗂️🤖 Airtable Model Context Protocol Server, for allowing AI systems to interact with your Airtable bases", "repository": { "url": "https://github.com/domdomegg/airtable-mcp-server", "source": "github", @@ -4618,21 +3687,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a4902ee8-060c-47d1-9dfc-ec6af1a9c398", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a4902ee8-060c-47d1-9dfc-ec6af1a9c398" - } - } + ] }, { "name": "io.github.felores/airtable-mcp", "description": "Search, create and update Airtable bases, tables, fields, and records using Claude Desktop and MCP (Model Context Protocol) clients", - "status": "active", "repository": { "url": "https://github.com/felores/airtable-mcp", "source": "github", @@ -4666,21 +3725,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "04216a87-0760-4f25-b809-42ffbfbb8962", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "04216a87-0760-4f25-b809-42ffbfbb8962" - } - } + ] }, { "name": "io.github.calvernaz/alphavantage", "description": "A MCP server for the stock market data API, Alphavantage API.", - "status": "active", "repository": { "url": "https://github.com/calvernaz/alphavantage", "source": "github", @@ -4697,21 +3746,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "cac9fd2a-2c08-441f-a94d-16c67821221f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "cac9fd2a-2c08-441f-a94d-16c67821221f" - } - } + ] }, { "name": "io.github.donghyun-chae/mcp-amadeus", "description": "Amadeus MCP(Model Context Protocol) Server", - "status": "active", "repository": { "url": "https://github.com/donghyun-chae/mcp-amadeus", "source": "github", @@ -4728,21 +3767,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ad6e04de-5f80-4274-9464-c6bbff533ad3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ad6e04de-5f80-4274-9464-c6bbff533ad3" - } - } + ] }, { "name": "io.github.diegobit/aranet4-mcp-server", "description": "SImple MCP server to manage your aranet4 device and local db.", - "status": "active", "repository": { "url": "https://github.com/diegobit/aranet4-mcp-server", "source": "github", @@ -4780,21 +3809,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "775bd83c-b33a-471c-9deb-8a444fb4ac79", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "775bd83c-b33a-471c-9deb-8a444fb4ac79" - } - } + ] }, { "name": "io.github.ravenwits/mcp-server-arangodb", "description": "This is a TypeScript-based MCP server that provides database interaction capabilities through ArangoDB. It implements core database operations and allows seamless integration with ArangoDB through MCP tools. You can use it wih Claude app and also extension for VSCode that works with mcp like Cline!", - "status": "active", "repository": { "url": "https://github.com/ravenwits/mcp-server-arangodb", "source": "github", @@ -4829,21 +3848,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1e97b930-031a-47b6-afb1-8af61dd81ec0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1e97b930-031a-47b6-afb1-8af61dd81ec0" - } - } + ] }, { "name": "io.github.vishalmysore/choturobo", "description": "Integrate Arduino-based robotics (using the NodeMCU ESP32 or Arduino Nano 368 board) with AI using the MCP (Model Context Protocol) framework from Claude Anthropic", - "status": "active", "repository": { "url": "https://github.com/vishalmysore/choturobo", "source": "github", @@ -4860,21 +3869,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "466926d1-72a6-42be-b2c1-ef94d44e5a67", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "466926d1-72a6-42be-b2c1-ef94d44e5a67" - } - } + ] }, { "name": "io.github.sooperset/mcp-atlassian", "description": "MCP server for Atlassian tools (Confluence, Jira)", - "status": "active", "repository": { "url": "https://github.com/sooperset/mcp-atlassian", "source": "github", @@ -4891,21 +3890,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4df1406c-1bb1-4495-96e8-8c25de454b5d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4df1406c-1bb1-4495-96e8-8c25de454b5d" - } - } + ] }, { "name": "io.github.co-browser/attestable-mcp-server", "description": "Verify that any MCP server is running the intended and untampered code via hardware attestation.", - "status": "active", "repository": { "url": "https://github.com/co-browser/attestable-mcp-server", "source": "github", @@ -4922,21 +3911,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "05ca6ccc-d6e1-47ab-ab8d-25949db9ada7", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "05ca6ccc-d6e1-47ab-ab8d-25949db9ada7" - } - } + ] }, { "name": "io.github.rishikavikondala/mcp-server-aws", "description": "A Model Context Protocol server implementation for operations on AWS resources", - "status": "active", "repository": { "url": "https://github.com/rishikavikondala/mcp-server-aws", "source": "github", @@ -4953,21 +3932,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e9799403-3917-4c38-9638-c69aebf1ac08", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e9799403-3917-4c38-9638-c69aebf1ac08" - } - } + ] }, { "name": "io.github.lishenxydlgzs/aws-athena-mcp", "description": "MCP server to run AWS Athena queries", - "status": "active", "repository": { "url": "https://github.com/lishenxydlgzs/aws-athena-mcp", "source": "github", @@ -5006,21 +3975,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d3139564-f4c3-44f9-902a-8d172038af01", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d3139564-f4c3-44f9-902a-8d172038af01" - } - } + ] }, { "name": "io.github.baryhuang/mcp-server-aws-resources-python", "description": "A Python-based MCP server that lets Claude run boto3 code to query and manage AWS resources. Execute powerful AWS operations directly through Claude with proper sandboxing and containerization. No need for complex setups - just pass your AWS credentials and start interacting with all AWS services.", - "status": "active", "repository": { "url": "https://github.com/baryhuang/mcp-server-aws-resources-python", "source": "github", @@ -5086,21 +4045,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6dd26dd7-d5c3-4aa8-bb9a-3aba328790ad", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6dd26dd7-d5c3-4aa8-bb9a-3aba328790ad" - } - } + ] }, { "name": "io.github.aws-samples/sample-mcp-server-s3", "description": "", - "status": "active", "repository": { "url": "https://github.com/aws-samples/sample-mcp-server-s3", "source": "github", @@ -5117,21 +4066,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f7f8acea-8725-48ec-a2f8-08ee16c738db", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f7f8acea-8725-48ec-a2f8-08ee16c738db" - } - } + ] }, { "name": "io.github.pab1it0/adx-mcp-server", "description": "A Model Context Protocol (MCP) server that enables AI assistants to query and analyze Azure Data Explorer databases through standardized interfaces.", - "status": "active", "repository": { "url": "https://github.com/pab1it0/adx-mcp-server", "source": "github", @@ -5251,21 +4190,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "bb734a22-a274-48ee-9740-d4a6a9a0a367", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "bb734a22-a274-48ee-9740-d4a6a9a0a367" - } - } + ] }, { "name": "io.github.vortiago/mcp-azure-devops", "description": "A Model Context Protocol (MCP) server enabling AI assistants to interact with Azure DevOps services via Python SDK.", - "status": "active", "repository": { "url": "https://github.com/Vortiago/mcp-azure-devops", "source": "github", @@ -5282,21 +4211,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4502246b-f419-4879-a179-149c6d87a871", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4502246b-f419-4879-a179-149c6d87a871" - } - } + ] }, { "name": "io.github.magnetai/mcp-free-usdc-transfer", "description": "MCP (Model Context Protocol) server - free usdc transfer powered by Coinbase CDP", - "status": "active", "repository": { "url": "https://github.com/magnetai/mcp-free-usdc-transfer", "source": "github", @@ -5323,21 +4242,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c3123664-33b3-4f29-bc08-40b88300f57c", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c3123664-33b3-4f29-bc08-40b88300f57c" - } - } + ] }, { "name": "io.github.basicmachines-co/basic-memory", "description": "Basic Memory is a knowledge management system that allows you to build a persistent semantic graph from conversations with AI assistants, stored in standard Markdown files on your computer. Integrates directly with Obsidan.md", - "status": "active", "repository": { "url": "https://github.com/basicmachines-co/basic-memory", "source": "github", @@ -5364,21 +4273,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "377dba9b-63cc-4ca9-bb39-97f5e12497b4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "377dba9b-63cc-4ca9-bb39-97f5e12497b4" - } - } + ] }, { "name": "io.github.lucashild/mcp-server-bigquery", "description": "A Model Context Protocol server that provides access to BigQuery", - "status": "active", "repository": { "url": "https://github.com/LucasHild/mcp-server-bigquery", "source": "github", @@ -5395,21 +4294,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0e517488-6e73-41c3-a297-7602f5937079", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0e517488-6e73-41c3-a297-7602f5937079" - } - } + ] }, { "name": "io.github.ergut/mcp-bigquery-server", "description": "A Model Context Protocol (MCP) server that provides secure, read-only access to BigQuery datasets. Enables Large Language Models (LLMs) to safely query and analyze data through a standardized interface.", - "status": "active", "repository": { "url": "https://github.com/ergut/mcp-bigquery-server", "source": "github", @@ -5458,21 +4347,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e3053bd5-1a4d-473c-a41b-f83ab1a46174", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e3053bd5-1a4d-473c-a41b-f83ab1a46174" - } - } + ] }, { "name": "io.github.leehanchung/bing-search-mcp", "description": "MCP Server for Bing Search API", - "status": "active", "repository": { "url": "https://github.com/leehanchung/bing-search-mcp", "source": "github", @@ -5495,21 +4374,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a0db598f-81d1-44d6-9008-e9a71e742cd3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a0db598f-81d1-44d6-9008-e9a71e742cd3" - } - } + ] }, { "name": "io.github.lloydzhou/bitable-mcp", "description": "This MCP server provides access to Lark Bitable through the Model Context Protocol. It allows users to interact with Bitable tables using predefined tools.", - "status": "active", "repository": { "url": "https://github.com/lloydzhou/bitable-mcp", "source": "github", @@ -5548,21 +4417,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4a33ebab-f5cf-48b3-adb3-3f7094ea706c", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4a33ebab-f5cf-48b3-adb3-3f7094ea706c" - } - } + ] }, { "name": "io.github.ahujasid/blender-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/ahujasid/blender-mcp", "source": "github", @@ -5597,21 +4456,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "fd341719-247e-45e8-a911-ad7c336eb163", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "fd341719-247e-45e8-a911-ad7c336eb163" - } - } + ] }, { "name": "io.github.agree-able/room-mcp", "description": "Allow MCP clients like claude-desktop to use rooms to coordinate with other agents", - "status": "active", "repository": { "url": "https://github.com/agree-able/room-mcp", "source": "github", @@ -5628,21 +4477,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b9bf1c69-4a64-47ec-a781-fb81d1922695", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b9bf1c69-4a64-47ec-a781-fb81d1922695" - } - } + ] }, { "name": "io.github.co-browser/browser-use-mcp-server", "description": "Browse the web, directly from Cursor etc.", - "status": "active", "repository": { "url": "https://github.com/co-browser/browser-use-mcp-server", "source": "github", @@ -5705,21 +4544,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "27b05d20-fd9c-437d-a86c-923629491ff5", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "27b05d20-fd9c-437d-a86c-923629491ff5" - } - } + ] }, { "name": "io.github.termix-official/bsc-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/TermiX-official/bsc-mcp", "source": "github", @@ -5736,21 +4565,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "36156772-6775-4b79-b560-b72ccaaca085", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "36156772-6775-4b79-b560-b72ccaaca085" - } - } + ] }, { "name": "io.github.githejie/mcp-server-calculator", "description": "A Model Context Protocol server for calculating.", - "status": "active", "repository": { "url": "https://github.com/githejie/mcp-server-calculator", "source": "github", @@ -5767,21 +4586,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "87cf029b-441f-439a-aae9-9ef27ddd4322", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "87cf029b-441f-439a-aae9-9ef27ddd4322" - } - } + ] }, { "name": "io.github.ai-ql/chat-mcp", "description": "A Desktop Chat App that leverages MCP(Model Context Protocol) to interface with other LLMs.", - "status": "active", "repository": { "url": "https://github.com/AI-QL/chat-mcp", "source": "github", @@ -5809,21 +4618,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "16fc441d-0d72-4b14-afd7-d5c9dd25c909", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "16fc441d-0d72-4b14-afd7-d5c9dd25c909" - } - } + ] }, { "name": "io.github.mcpso/mcp-server-chatsum", "description": "Query and Summarize your chat messages.", - "status": "active", "repository": { "url": "https://github.com/chatmcp/mcp-server-chatsum", "source": "github", @@ -5846,21 +4645,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "196a8cbd-8234-4336-a4d7-f6dda870bcfb", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "196a8cbd-8234-4336-a4d7-f6dda870bcfb" - } - } + ] }, { "name": "io.github.privetin/chroma", "description": "MCP server for Chroma", - "status": "active", "repository": { "url": "https://github.com/privetin/chroma", "source": "github", @@ -5898,21 +4687,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4b637a92-c1af-463d-afc1-e0ce65d7c057", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4b637a92-c1af-463d-afc1-e0ce65d7c057" - } - } + ] }, { "name": "io.github.zilongxue/claude-post", "description": "ClaudePost enables seamless email management through natural language conversations with Claude, offering secure features like email search, reading, and sending.", - "status": "active", "repository": { "url": "https://github.com/ZilongXue/claude-post", "source": "github", @@ -5950,21 +4729,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3b404956-9bad-4bd9-89a9-17534b4bd22e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3b404956-9bad-4bd9-89a9-17534b4bd22e" - } - } + ] }, { "name": "io.github.taazkareem/clickup-mcp-server", "description": "ClickUp MCP Server - Integrate ClickUp task management with AI through Model Context Protocol", - "status": "active", "repository": { "url": "https://github.com/taazkareem/clickup-mcp-server", "source": "github", @@ -5999,21 +4768,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "34b93e72-0fa8-4dbe-ae9b-82a9e40de735", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "34b93e72-0fa8-4dbe-ae9b-82a9e40de735" - } - } + ] }, { "name": "io.github.felores/cloudinary-mcp-server", "description": "MCP (Model Context Protocol) server for uploading media to Cloudinary using Claude Desktop", - "status": "active", "repository": { "url": "https://github.com/felores/cloudinary-mcp-server", "source": "github", @@ -6055,21 +4814,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7baddfbe-3618-41d8-befe-40fc2aabcd8e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7baddfbe-3618-41d8-befe-40fc2aabcd8e" - } - } + ] }, { "name": "io.github.bazinga012/mcp_code_executor", "description": "The MCP Code Executor is an MCP server that allows LLMs to execute Python code within a specified Conda environment.", - "status": "active", "repository": { "url": "https://github.com/bazinga012/mcp_code_executor", "source": "github", @@ -6097,21 +4846,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0cdfed38-6495-42a3-a837-3788f35aadd3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0cdfed38-6495-42a3-a837-3788f35aadd3" - } - } + ] }, { "name": "io.github.kocierik/consul-mcp-server", "description": "A consul MCP Server (modelcontextprotocol) ", - "status": "active", "repository": { "url": "https://github.com/kocierik/consul-mcp-server", "source": "github", @@ -6128,21 +4867,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a9fe34f1-7c82-4f7d-8fbe-20bb8ca29e7b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a9fe34f1-7c82-4f7d-8fbe-20bb8ca29e7b" - } - } + ] }, { "name": "io.github.topoteretes/cognee", "description": "Memory for AI Agents in 5 lines of code", - "status": "active", "repository": { "url": "https://github.com/topoteretes/cognee", "source": "github", @@ -6159,21 +4888,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ae9ce022-800d-4a10-a452-5bc396d25430", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ae9ce022-800d-4a10-a452-5bc396d25430" - } - } + ] }, { "name": "io.github.longmans/coin_api_mcp", "description": "A Model Context Protocol server that provides access to CoinMarketCap's cryptocurrency data. This server enables AI-powered applications to retrieve cryptocurrency listings, quotes, and detailed information about various coins.", - "status": "active", "repository": { "url": "https://github.com/longmans/coin_api_mcp", "source": "github", @@ -6208,21 +4927,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "28fc08e1-1afc-42cb-8eca-0b500e23c73e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "28fc08e1-1afc-42cb-8eca-0b500e23c73e" - } - } + ] }, { "name": "io.github.shinzo-labs/coinmarketcap-mcp", "description": "MCP Implementation for CoinMarketCap", - "status": "active", "repository": { "url": "https://github.com/shinzo-labs/coinmarketcap-mcp", "source": "github", @@ -6239,21 +4948,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7e3a17ce-fbbd-48cb-b29f-34a4b8c6ca55", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7e3a17ce-fbbd-48cb-b29f-34a4b8c6ca55" - } - } + ] }, { "name": "io.github.baryhuang/mcp-remote-macos-use", "description": "The only general AI agent that does NOT requires extra API key, giving you full control on your local and remote MacOs from Claude Desktop App", - "status": "active", "repository": { "url": "https://github.com/baryhuang/mcp-remote-macos-use", "source": "github", @@ -6321,21 +5020,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5e32ec37-7a46-4e84-9f19-845cd6b151f4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5e32ec37-7a46-4e84-9f19-845cd6b151f4" - } - } + ] }, { "name": "io.github.ivo-toby/contentful-mcp", "description": "MCP (Model Context Protocol) server for the Contentful Management API", - "status": "active", "repository": { "url": "https://github.com/ivo-toby/contentful-mcp", "source": "github", @@ -6374,21 +5063,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "347d6881-c509-4dca-9ece-95e25098436b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "347d6881-c509-4dca-9ece-95e25098436b" - } - } + ] }, { "name": "io.github.kukapay/crypto-feargreed-mcp", "description": "Providing real-time and historical Crypto Fear & Greed Index data", - "status": "active", "repository": { "url": "https://github.com/kukapay/crypto-feargreed-mcp", "source": "github", @@ -6405,21 +5084,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "45ee52ba-1e4c-48a5-9eef-de7a234997db", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "45ee52ba-1e4c-48a5-9eef-de7a234997db" - } - } + ] }, { "name": "io.github.kukapay/crypto-indicators-mcp", "description": "An MCP server providing a range of cryptocurrency technical analysis indicators and strategies.", - "status": "active", "repository": { "url": "https://github.com/kukapay/crypto-indicators-mcp", "source": "github", @@ -6453,21 +5122,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0852caec-a861-4f9c-9e94-a7c8b67a1641", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0852caec-a861-4f9c-9e94-a7c8b67a1641" - } - } + ] }, { "name": "io.github.kukapay/cryptopanic-mcp-server", "description": "Provide latest cryptocurrency news to AI agents.", - "status": "active", "repository": { "url": "https://github.com/kukapay/cryptopanic-mcp-server", "source": "github", @@ -6484,21 +5143,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "91f75500-0051-4d05-a5b2-9a87aa52ec3a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "91f75500-0051-4d05-a5b2-9a87aa52ec3a" - } - } + ] }, { "name": "io.github.dappierai/dappier-mcp", - "description": "Enable fast, free real-time web search and access premium data from trusted media brands\u2014news, financial markets, sports, entertainment, weather, and more. Build powerful AI agents with Dappier.", - "status": "active", + "description": "Enable fast, free real-time web search and access premium data from trusted media brands—news, financial markets, sports, entertainment, weather, and more. Build powerful AI agents with Dappier.", "repository": { "url": "https://github.com/DappierAI/dappier-mcp", "source": "github", @@ -6521,21 +5170,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "58b5f6c8-1ad7-4378-9921-7b9b7982c84c", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "58b5f6c8-1ad7-4378-9921-7b9b7982c84c" - } - } + ] }, { "name": "io.github.geli2001/datadog-mcp-server", "description": "MCP server interacts with the official Datadog API", - "status": "active", "repository": { "url": "https://github.com/GeLi2001/datadog-mcp-server", "source": "github", @@ -6604,21 +5243,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "24f2d280-583d-4511-af0c-47910fc52c4d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "24f2d280-583d-4511-af0c-47910fc52c4d" - } - } + ] }, { "name": "io.github.reading-plus-ai/mcp-server-data-exploration", "description": "", - "status": "active", "repository": { "url": "https://github.com/reading-plus-ai/mcp-server-data-exploration", "source": "github", @@ -6635,21 +5264,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "14074b80-f3a7-4b45-97ba-7204c92c5f6e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "14074b80-f3a7-4b45-97ba-7204c92c5f6e" - } - } + ] }, { "name": "io.github.privetin/dataset-viewer", "description": "MCP server for Hugging Face dataset viewer", - "status": "active", "repository": { "url": "https://github.com/privetin/dataset-viewer", "source": "github", @@ -6676,21 +5295,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "200d7310-6d18-4b22-98b3-68ae63ea985a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "200d7310-6d18-4b22-98b3-68ae63ea985a" - } - } + ] }, { "name": "io.github.snagasuri/deebo-prototype", "description": "Autonomous debugging agent MCP server", - "status": "active", "repository": { "url": "https://github.com/snagasuri/deebo-prototype", "source": "github", @@ -6776,21 +5385,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ec2849c6-e8ed-46b1-8e57-646fd2e8be93", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ec2849c6-e8ed-46b1-8e57-646fd2e8be93" - } - } + ] }, { "name": "io.github.reading-plus-ai/mcp-server-deep-research", "description": "", - "status": "active", "repository": { "url": "https://github.com/reading-plus-ai/mcp-server-deep-research", "source": "github", @@ -6807,21 +5406,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "79f72036-83ee-4420-9767-980e27590e6f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "79f72036-83ee-4420-9767-980e27590e6f" - } - } + ] }, { "name": "io.github.dmontgomery40/deepseek-mcp-server", "description": "Model Context Protocol server for DeepSeek's advanced language models", - "status": "active", "repository": { "url": "https://github.com/DMontgomery40/deepseek-mcp-server", "source": "github", @@ -6844,21 +5433,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "345bf964-02ce-4814-b37f-52c2e73ab395", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "345bf964-02ce-4814-b37f-52c2e73ab395" - } - } + ] }, { "name": "io.github.66julienmartin/mcp-server-deepseek_r1", "description": "A Model Context Protocol (MCP) server implementation connecting Claude Desktop with DeepSeek's language models (R1/V3)", - "status": "active", "repository": { "url": "https://github.com/66julienmartin/MCP-server-Deepseek_R1", "source": "github", @@ -6881,21 +5460,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a2015046-780d-421a-999f-c52c808236fa", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a2015046-780d-421a-999f-c52c808236fa" - } - } + ] }, { "name": "io.github.ruixingshi/deepseek-thinker-mcp", "description": "A MCP provider Deepseek reasoning content to MCP-enabled AI Clients, like Claude Desktop. Supports access to Deepseek's CoT from the Deepseek API service or a local Ollama server.", - "status": "active", "repository": { "url": "https://github.com/ruixingshi/deepseek-thinker-mcp", "source": "github", @@ -6922,21 +5491,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "883a7c72-e6ca-41cb-91e9-e65436a58729", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "883a7c72-e6ca-41cb-91e9-e65436a58729" - } - } + ] }, { "name": "io.github.descope-sample-apps/descope-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/descope-sample-apps/descope-mcp-server-stdio", "source": "github", @@ -6963,21 +5522,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "bac14e5d-08c4-4b69-8e40-b845632d26ed", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "bac14e5d-08c4-4b69-8e40-b845632d26ed" - } - } + ] }, { "name": "io.github.kpsunil97/devrev-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/kpsunil97/devrev-mcp-server", "source": "github", @@ -6994,21 +5543,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "8b17658e-2a1b-4ca7-a1ae-583e7b190bce", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "8b17658e-2a1b-4ca7-a1ae-583e7b190bce" - } - } + ] }, { "name": "io.github.christianhinge/dicom-mcp", "description": "Model Context Protocol (MCP) for interacting with dicom servers (PACS etc.)", - "status": "active", "repository": { "url": "https://github.com/ChristianHinge/dicom-mcp", "source": "github", @@ -7055,21 +5594,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c83f2ca0-ca97-43cf-8288-5ff4a0771e29", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c83f2ca0-ca97-43cf-8288-5ff4a0771e29" - } - } + ] }, { "name": "io.github.v-3/discordmcp", "description": "Discord MCP Server for Claude Integration", - "status": "active", "repository": { "url": "https://github.com/v-3/discordmcp", "source": "github", @@ -7086,21 +5615,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "361ed7f4-8df4-4a8e-931f-edfb380e0019", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "361ed7f4-8df4-4a8e-931f-edfb380e0019" - } - } + ] }, { "name": "io.github.ashdevfr/discourse-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/AshDevFr/discourse-mcp-server", "source": "github", @@ -7131,21 +5650,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "dedc68af-03ef-49ce-a331-e4717b96f693", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "dedc68af-03ef-49ce-a331-e4717b96f693" - } - } + ] }, { "name": "io.github.ckreiling/mcp-server-docker", "description": "MCP server for Docker", - "status": "active", "repository": { "url": "https://github.com/ckreiling/mcp-server-docker", "source": "github", @@ -7162,21 +5671,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d2aacd5b-0642-4832-8629-58272a922e4b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d2aacd5b-0642-4832-8629-58272a922e4b" - } - } + ] }, { "name": "io.github.szeider/mcp-dblp", "description": "A Model Context Protocol (MCP) server that provides access to the DBLP computer science bibliography database for Large Language Models.", - "status": "active", "repository": { "url": "https://github.com/szeider/mcp-dblp", "source": "github", @@ -7193,21 +5692,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5a9955bc-6c28-4ebd-974e-73226eb3e58f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5a9955bc-6c28-4ebd-974e-73226eb3e58f" - } - } + ] }, { "name": "io.github.omedia/mcp-server-drupal", "description": "TS based companion MCP server for the Drupal MCP module that works with the STDIO transport.", - "status": "active", "repository": { "url": "https://github.com/Omedia/mcp-server-drupal", "source": "github", @@ -7224,21 +5713,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7902d399-3080-4926-afb8-4f98c3a7d870", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7902d399-3080-4926-afb8-4f98c3a7d870" - } - } + ] }, { "name": "io.github.kukapay/dune-analytics-mcp", "description": "A mcp server that bridges Dune Analytics data to AI agents.", - "status": "active", "repository": { "url": "https://github.com/kukapay/dune-analytics-mcp", "source": "github", @@ -7255,21 +5734,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2d851046-40f2-4088-95df-d6c0f93c2f0a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2d851046-40f2-4088-95df-d6c0f93c2f0a" - } - } + ] }, { "name": "io.github.tencentedgeone/edgeone-pages-mcp", "description": "An MCP service designed for deploying HTML content to EdgeOne Pages and obtaining an accessible public URL.", - "status": "active", "repository": { "url": "https://github.com/TencentEdgeOne/edgeone-pages-mcp", "source": "github", @@ -7286,21 +5755,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f97607ba-eb50-4258-b46c-ce9fc6587fa9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f97607ba-eb50-4258-b46c-ce9fc6587fa9" - } - } + ] }, { "name": "io.github.edwin-finance/edwin", "description": "Empowering AI agents to dominate DeFAI", - "status": "active", "repository": { "url": "https://github.com/edwin-finance/edwin", "source": "github", @@ -7317,21 +5776,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "995ac56a-5ec7-4411-a86c-4ebfef9c9605", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "995ac56a-5ec7-4411-a86c-4ebfef9c9605" - } - } + ] }, { "name": "io.github.mamertofabian/elevenlabs-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/mamertofabian/elevenlabs-mcp-server", "source": "github", @@ -7399,21 +5848,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1d89482a-a7de-4d3b-a7cb-1b7be49939ec", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1d89482a-a7de-4d3b-a7cb-1b7be49939ec" - } - } + ] }, { "name": "io.github.shy2593666979/mcp-server-email", "description": "", - "status": "active", "repository": { "url": "https://github.com/Shy2593666979/mcp-server-email", "source": "github", @@ -7430,21 +5869,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c0a64e6c-2ff2-4a2f-afe6-f52119a020a0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c0a64e6c-2ff2-4a2f-afe6-f52119a020a0" - } - } + ] }, { "name": "io.github.whataboutyou-ai/eunomia-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/whataboutyou-ai/eunomia-mcp-server", "source": "github", @@ -7461,21 +5890,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "35e1e1a0-30c9-4857-a9e0-5c64f7e29fa9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "35e1e1a0-30c9-4857-a9e0-5c64f7e29fa9" - } - } + ] }, { "name": "io.github.mcpdotdirect/evm-mcp-server", "description": "MCP server that provides LLM with tools for interacting with EVM networks", - "status": "active", "repository": { "url": "https://github.com/mcpdotdirect/evm-mcp-server", "source": "github", @@ -7492,21 +5911,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e944c7a9-9eb0-495a-8bee-d8da751240e7", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e944c7a9-9eb0-495a-8bee-d8da751240e7" - } - } + ] }, { "name": "io.github.mamertofabian/mcp-everything-search", "description": "", - "status": "active", "repository": { "url": "https://github.com/mamertofabian/mcp-everything-search", "source": "github", @@ -7550,21 +5959,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c6f8403f-30d0-45d2-b7c3-95a6cb61c4b4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c6f8403f-30d0-45d2-b7c3-95a6cb61c4b4" - } - } + ] }, { "name": "io.github.haris-musa/excel-mcp-server", "description": "A Model Context Protocol server for Excel file manipulation", - "status": "active", "repository": { "url": "https://github.com/haris-musa/excel-mcp-server", "source": "github", @@ -7587,21 +5986,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2a5c04e4-0526-492b-aa98-49896e033b74", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2a5c04e4-0526-492b-aa98-49896e033b74" - } - } + ] }, { "name": "io.github.rishijatia/fantasy-pl-mcp", "description": "Fantasy Premier League MCP Server", - "status": "active", "repository": { "url": "https://github.com/rishijatia/fantasy-pl-mcp", "source": "github", @@ -7618,21 +6007,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f1da402e-a006-4364-a100-5131eb05b579", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f1da402e-a006-4364-a100-5131eb05b579" - } - } + ] }, { "name": "io.github.fastnai/mcp-fastn", "description": "", - "status": "active", "repository": { "url": "https://github.com/fastnai/mcp-fastn", "source": "github", @@ -7659,21 +6038,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3d7c6822-88bd-4569-a099-1e58b9b365b0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3d7c6822-88bd-4569-a099-1e58b9b365b0" - } - } + ] }, { "name": "io.github.stefanoamorelli/fred-mcp-server", "description": "Open-source MCP Server for the Federal Reserve Economic Data", - "status": "active", "repository": { "url": "https://github.com/stefanoamorelli/fred-mcp-server", "source": "github", @@ -7696,21 +6065,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "2382172a-b26a-40fc-adee-f82132b56edc", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "2382172a-b26a-40fc-adee-f82132b56edc" - } - } + ] }, { "name": "io.github.zcaceres/fetch-mcp", "description": "A flexible HTTP fetching Model Context Protocol server.", - "status": "active", "repository": { "url": "https://github.com/zcaceres/fetch-mcp", "source": "github", @@ -7727,21 +6086,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c2f4ab61-b279-4cb9-932d-5f60bcd1534a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c2f4ab61-b279-4cb9-932d-5f60bcd1534a" - } - } + ] }, { "name": "io.github.glips/figma-context-mcp", "description": "MCP server to provide Figma layout information to AI coding agents like Cursor", - "status": "active", "repository": { "url": "https://github.com/GLips/Figma-Context-MCP", "source": "github", @@ -7779,21 +6128,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e61c643c-ec01-483c-a586-20f87ab6280e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e61c643c-ec01-483c-a586-20f87ab6280e" - } - } + ] }, { "name": "io.github.gannonh/firebase-mcp", - "description": "\ud83d\udd25 Model Context Protocol (MCP) server for Firebase.", - "status": "active", + "description": "🔥 Model Context Protocol (MCP) server for Firebase.", "repository": { "url": "https://github.com/gannonh/firebase-mcp", "source": "github", @@ -7810,21 +6149,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d7a1d303-082e-477d-8ab5-9d65dc0117d9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d7a1d303-082e-477d-8ab5-9d65dc0117d9" - } - } + ] }, { "name": "io.github.vrknetha/mcp-server-firecrawl", "description": "Official Firecrawl MCP Server - Adds powerful web scraping to Cursor, Claude and any other LLM clients.", - "status": "active", "repository": { "url": "https://github.com/mendableai/firecrawl-mcp-server", "source": "github", @@ -7841,21 +6170,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3947e186-beb0-4372-8f74-045047dacebd", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3947e186-beb0-4372-8f74-045047dacebd" - } - } + ] }, { "name": "io.github.sunsetcoder/flightradar24-mcp-server", "description": "Model Context Protocol server for Flight Tracking", - "status": "active", "repository": { "url": "https://github.com/sunsetcoder/flightradar24-mcp-server", "source": "github", @@ -7882,21 +6201,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "50b7ab52-c3d0-40bc-b342-38fa93da54ff", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "50b7ab52-c3d0-40bc-b342-38fa93da54ff" - } - } + ] }, { "name": "io.github.kukapay/freqtrade-mcp", "description": "An MCP server that integrates with the Freqtrade cryptocurrency trading bot.", - "status": "active", "repository": { "url": "https://github.com/kukapay/freqtrade-mcp", "source": "github", @@ -7913,21 +6222,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "573c235f-93d7-45a3-9f23-b1166a660c97", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "573c235f-93d7-45a3-9f23-b1166a660c97" - } - } + ] }, { "name": "io.github.mfydev/ghost-mcp", "description": "A Model Context Protocol (MCP) server for interacting with Ghost CMS through LLM interfaces like Claude. Allow you to control your Ghost blog by simply asking Claude etc.", - "status": "active", "repository": { "url": "https://github.com/MFYDev/ghost-mcp", "source": "github", @@ -7958,21 +6257,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e4927319-b643-498e-935f-51bad4e30381", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e4927319-b643-498e-935f-51bad4e30381" - } - } + ] }, { "name": "io.github.ko1ynnky/github-actions-mcp-server", "description": "GitHub Actions Model Context Protocol Server", - "status": "active", "repository": { "url": "https://github.com/ko1ynnky/github-actions-mcp-server", "source": "github", @@ -8006,21 +6295,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "fa6d267b-a797-42b7-9510-5abc3c518f3e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "fa6d267b-a797-42b7-9510-5abc3c518f3e" - } - } + ] }, { "name": "io.github.ddukbg/github-enterprise-mcp", "description": "github-enterprise-mcp", - "status": "active", "repository": { "url": "https://github.com/containerelic/github-enterprise-mcp", "source": "github", @@ -8047,21 +6326,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "cc190d52-686a-4286-812d-c4542161203d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "cc190d52-686a-4286-812d-c4542161203d" - } - } + ] }, { "name": "io.github.gongrzhe/gmail-mcp-server", "description": "A Model Context Protocol (MCP) server for Gmail integration in Claude Desktop with auto authentication support. This server enables AI assistants to manage Gmail through natural language interactions.", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/Gmail-MCP-Server", "source": "github", @@ -8078,21 +6347,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "36ad8cdb-408e-4110-827b-8d6a0a0f94bb", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "36ad8cdb-408e-4110-827b-8d6a0a0f94bb" - } - } + ] }, { "name": "io.github.baryhuang/mcp-headless-gmail", "description": "A MCP (Model Context Protocol) server that provides get, send Gmails without local credential or token setup.", - "status": "active", "repository": { "url": "https://github.com/baryhuang/mcp-headless-gmail", "source": "github", @@ -8156,21 +6415,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "83932f7e-8c4d-475b-b03b-f30bc31793d3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "83932f7e-8c4d-475b-b03b-f30bc31793d3" - } - } + ] }, { "name": "io.github.hichana/goalstory-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/hichana/goalstory-mcp", "source": "github", @@ -8187,21 +6436,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "21242e10-3373-40a3-9119-e4d075b7c27e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "21242e10-3373-40a3-9119-e4d075b7c27e" - } - } + ] }, { "name": "io.github.goat-sdk/goat", "description": "The leading agentic finance toolkit for AI agents", - "status": "active", "repository": { "url": "https://github.com/goat-sdk/goat", "source": "github", @@ -8218,21 +6457,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6e61849a-2312-4c76-a9d0-0a4b84a7795e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6e61849a-2312-4c76-a9d0-0a4b84a7795e" - } - } + ] }, { "name": "io.github.coding-solo/godot-mcp", "description": "MCP server for interfacing with Godot game engine. Provides tools for launching the editor, running projects, and capturing debug output.", - "status": "active", "repository": { "url": "https://github.com/Coding-Solo/godot-mcp", "source": "github", @@ -8255,21 +6484,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "8663db3e-ad80-4805-8032-11a3f303b5fc", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "8663db3e-ad80-4805-8032-11a3f303b5fc" - } - } + ] }, { "name": "io.github.mark3labs/mcp-filesystem-server", "description": "Go server implementing Model Context Protocol (MCP) for filesystem operations.", - "status": "active", "repository": { "url": "https://github.com/mark3labs/mcp-filesystem-server", "source": "github", @@ -8306,21 +6525,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "93c84b33-4392-41b2-85be-764e5f39da74", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "93c84b33-4392-41b2-85be-764e5f39da74" - } - } + ] }, { "name": "io.github.vectorinstitute/mcp-goodnews", "description": "A simple MCP application that delivers curated positive and uplifting news stories.", - "status": "active", "repository": { "url": "https://github.com/VectorInstitute/mcp-goodnews", "source": "github", @@ -8368,21 +6577,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "91c0fc60-7dc9-4095-a05a-9f816738df67", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "91c0fc60-7dc9-4095-a05a-9f816738df67" - } - } + ] }, { "name": "io.github.v-3/google-calendar", "description": "", - "status": "active", "repository": { "url": "https://github.com/v-3/google-calendar", "source": "github", @@ -8399,21 +6598,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "165ef95f-bfb3-4b08-b523-d17748fdfe31", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "165ef95f-bfb3-4b08-b523-d17748fdfe31" - } - } + ] }, { "name": "io.github.nspady/google-calendar-mcp", "description": "MCP integration for Google Calendar to manage events.", - "status": "active", "repository": { "url": "https://github.com/nspady/google-calendar-mcp", "source": "github", @@ -8430,21 +6619,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6543c44a-7ad6-4f9e-bf7e-9f8f969934bf", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6543c44a-7ad6-4f9e-bf7e-9f8f969934bf" - } - } + ] }, { "name": "io.github.adenot/mcp-google-search", "description": "", - "status": "active", "repository": { "url": "https://github.com/adenot/mcp-google-search", "source": "github", @@ -8461,21 +6640,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d5076249-cb9d-4c5e-ae2e-544779ec1a04", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d5076249-cb9d-4c5e-ae2e-544779ec1a04" - } - } + ] }, { "name": "io.github.xing5/mcp-google-sheets", "description": "This MCP server integrates with your Google Drive and Google Sheets, to enable creating and modifying spreadsheets.", - "status": "active", "repository": { "url": "https://github.com/xing5/mcp-google-sheets", "source": "github", @@ -8513,21 +6682,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "69ae0fdb-713d-4f8e-bf8e-d214e79fa295", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "69ae0fdb-713d-4f8e-bf8e-d214e79fa295" - } - } + ] }, { "name": "io.github.rohans2/mcp-google-sheets", "description": "", - "status": "active", "repository": { "url": "https://github.com/rohans2/mcp-google-sheets", "source": "github", @@ -8555,21 +6714,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b1051584-3b09-49af-a97f-a92b47861ed1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b1051584-3b09-49af-a97f-a92b47861ed1" - } - } + ] }, { "name": "io.github.ubie-oss/mcp-vertexai-search", "description": "A MCP server for Vertex AI Search", - "status": "active", "repository": { "url": "https://github.com/ubie-oss/mcp-vertexai-search", "source": "github", @@ -8586,21 +6735,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "07ccf835-680a-42b4-bf0b-db28367519ee", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "07ccf835-680a-42b4-bf0b-db28367519ee" - } - } + ] }, { "name": "io.github.hannesj/mcp-graphql-schema", "description": "GraphQL Schema Model Context Protocol Server", - "status": "active", "repository": { "url": "https://github.com/hannesj/mcp-graphql-schema", "source": "github", @@ -8628,21 +6767,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7461c9c2-108f-46c1-8f35-6206ec509267", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7461c9c2-108f-46c1-8f35-6206ec509267" - } - } + ] }, { "name": "io.github.horizondatawave/hdw-mcp-server", "description": "A Model Context Protocol (MCP) server that provides comprehensive access to LinkedIn data and functionalities using the HorizonDataWave API, enabling not only data retrieval but also robust management of user accounts.", - "status": "active", "repository": { "url": "https://github.com/horizondatawave/hdw-mcp-server", "source": "github", @@ -8669,21 +6798,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0f4331b9-9229-49c4-b41c-5c254b610daa", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0f4331b9-9229-49c4-b41c-5c254b610daa" - } - } + ] }, { "name": "io.github.heurist-network/heurist-mesh-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/heurist-network/heurist-mesh-mcp-server", "source": "github", @@ -8700,21 +6819,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "97174daf-db12-40ac-bab2-1c4255ce1741", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "97174daf-db12-40ac-bab2-1c4255ce1741" - } - } + ] }, { "name": "io.github.syucream/holaspirit-mcp-server", "description": "A MCP server that accesses to Holaspirit", - "status": "active", "repository": { "url": "https://github.com/syucream/holaspirit-mcp-server", "source": "github", @@ -8741,21 +6850,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "fffcda50-ddc1-4357-a9bf-b7022f86c582", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "fffcda50-ddc1-4357-a9bf-b7022f86c582" - } - } + ] }, { "name": "io.github.tevonsb/homeassistant-mcp", "description": "A MCP server for Home Assistant", - "status": "active", "repository": { "url": "https://github.com/tevonsb/homeassistant-mcp", "source": "github", @@ -8772,21 +6871,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b87405fe-1aae-4a7a-a4d7-cd0b5f267ad8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b87405fe-1aae-4a7a-a4d7-cd0b5f267ad8" - } - } + ] }, { "name": "io.github.voska/hass-mcp", "description": "Home Assistant MCP Server", - "status": "active", "repository": { "url": "https://github.com/voska/hass-mcp", "source": "github", @@ -8862,21 +6951,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "67c71178-8632-4d67-abad-c28f6e58c87f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "67c71178-8632-4d67-abad-c28f6e58c87f" - } - } + ] }, { "name": "io.github.buryhuang/mcp-hubspot", "description": "A Model Context Protocol (MCP) server that enables AI assistants to interact with HubSpot CRM data, providing built-in vector storage and caching mechanisms help overcome HubSpot API limitations while improving response times.", - "status": "active", "repository": { "url": "https://github.com/peakmojo/mcp-hubspot", "source": "github", @@ -8943,21 +7022,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "aef75131-e237-4b91-a2b8-c0f416d3618f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "aef75131-e237-4b91-a2b8-c0f416d3618f" - } - } + ] }, { "name": "io.github.evalstate/mcp-hfspace", "description": "MCP Server to Use HuggingFace spaces, easy configuration and Claude Desktop mode. ", - "status": "active", "repository": { "url": "https://github.com/evalstate/mcp-hfspace", "source": "github", @@ -9023,21 +7092,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "532d7690-7a65-470f-81da-2ba3042bee05", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "532d7690-7a65-470f-81da-2ba3042bee05" - } - } + ] }, { "name": "io.github.mektigboy/server-hyperliquid", "description": "MCP Hyperliquid (https://app.hyperliquid.xyz) server", - "status": "active", "repository": { "url": "https://github.com/mektigboy/server-hyperliquid", "source": "github", @@ -9054,21 +7113,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0d57edf7-3fbb-42b4-aa9e-b255e7d6f8fa", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0d57edf7-3fbb-42b4-aa9e-b255e7d6f8fa" - } - } + ] }, { "name": "io.github.iflytek/ifly-workflow-mcp-server", "description": "This a simple implementation of an MCP server using iFlytek. It enables calling iFlytek workflows through MCP tools.", - "status": "active", "repository": { "url": "https://github.com/iflytek/ifly-workflow-mcp-server", "source": "github", @@ -9112,21 +7161,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6a9b532e-61df-4d4d-a898-25d66aa60315", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6a9b532e-61df-4d4d-a898-25d66aa60315" - } - } + ] }, { "name": "io.github.gongrzhe/image-generation-mcp-server", "description": "This MCP server provides image generation capabilities using the Replicate Flux model.", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/Image-Generation-MCP-Server", "source": "github", @@ -9153,21 +7192,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5bf6d045-dc83-4b51-858f-eb9c3403087f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5bf6d045-dc83-4b51-858f-eb9c3403087f" - } - } + ] }, { "name": "io.github.idoru/influxdb-mcp-server", "description": "An MCP Server for querying InfluxDB", - "status": "active", "repository": { "url": "https://github.com/idoru/influxdb-mcp-server", "source": "github", @@ -9198,21 +7227,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d78df157-559d-4e87-af1f-0dcbfe703a23", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d78df157-559d-4e87-af1f-0dcbfe703a23" - } - } + ] }, { "name": "io.github.sergehuber/inoyu-mcp-unomi-server", "description": "An implementation of Anthropic's Model Context Protocol for the Apache Unomi CDP", - "status": "active", "repository": { "url": "https://github.com/sergehuber/inoyu-mcp-unomi-server", "source": "github", @@ -9229,21 +7248,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "601ee7e9-e0db-470e-80e5-f1509cddf6d7", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "601ee7e9-e0db-470e-80e5-f1509cddf6d7" - } - } + ] }, { "name": "io.github.raoulbia-ai/mcp-server-for-intercom", "description": "", - "status": "active", "repository": { "url": "https://github.com/raoulbia-ai/mcp-server-for-intercom", "source": "github", @@ -9266,21 +7275,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1967f4e6-f229-4d5f-a648-9bdc67d39da8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1967f4e6-f229-4d5f-a648-9bdc67d39da8" - } - } + ] }, { "name": "io.github.inditextech/mcp-server-simulator-ios-idb", "description": "A Model Context Protocol (MCP) server that enables LLMs to interact with iOS simulators through natural language commands.", - "status": "active", "repository": { "url": "https://github.com/InditexTech/mcp-server-simulator-ios-idb", "source": "github", @@ -9297,21 +7296,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b9fa6e10-c6f4-424d-b289-469d49decaaf", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b9fa6e10-c6f4-424d-b289-469d49decaaf" - } - } + ] }, { "name": "io.github.ferrislucas/iterm-mcp", "description": "A Model Context Protocol server that executes commands in the current iTerm session - useful for REPL and CLI assistance", - "status": "active", "repository": { "url": "https://github.com/ferrislucas/iterm-mcp", "source": "github", @@ -9328,21 +7317,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "dcac9b0e-b18c-4570-947c-7a6d351b02df", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "dcac9b0e-b18c-4570-947c-7a6d351b02df" - } - } + ] }, { "name": "io.github.rishabkoul/iterm-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/rishabkoul/iTerm-MCP-Server", "source": "github", @@ -9359,21 +7338,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "add3f73d-394b-4b99-b255-8d9d89db84c2", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "add3f73d-394b-4b99-b255-8d9d89db84c2" - } - } + ] }, { "name": "io.github.qainsights/jmeter-mcp-server", - "description": "\u2728 JMeter Meets AI Workflows: Introducing the JMeter MCP Server! \ud83e\udd2f ", - "status": "active", + "description": "✨ JMeter Meets AI Workflows: Introducing the JMeter MCP Server! 🤯 ", "repository": { "url": "https://github.com/QAInsights/jmeter-mcp-server", "source": "github", @@ -9400,21 +7369,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d0f30dc9-bc11-43f8-9b61-034e2bec78d5", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d0f30dc9-bc11-43f8-9b61-034e2bec78d5" - } - } + ] }, { "name": "io.github.gongrzhe/json-mcp-server", "description": "JSON handling and processing mcp server", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/JSON-MCP-Server", "source": "github", @@ -9431,21 +7390,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7e73a81a-e914-4cf7-aff2-482d0047e8dd", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7e73a81a-e914-4cf7-aff2-482d0047e8dd" - } - } + ] }, { "name": "io.github.kukapay/jupiter-mcp", "description": "An MCP server for executing token swaps on the Solana blockchain using Jupiter's new Ultra API.", - "status": "active", "repository": { "url": "https://github.com/kukapay/jupiter-mcp", "source": "github", @@ -9483,21 +7432,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "243a8c0e-4bb7-4932-9d7c-8e8229306295", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "243a8c0e-4bb7-4932-9d7c-8e8229306295" - } - } + ] }, { "name": "io.github.christophenglisch/keycloak-model-context-protocol", "description": "MCP server implementation for Keycloak user management. Enables AI-powered administration of Keycloak users and realms through the Model Context Protocol (MCP). Seamlessly integrates with Claude Desktop and other MCP clients for automated user operations.", - "status": "active", "repository": { "url": "https://github.com/ChristophEnglisch/keycloak-model-context-protocol", "source": "github", @@ -9539,21 +7478,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ac8e4d2b-da97-4a40-934b-1c516b4250fe", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ac8e4d2b-da97-4a40-934b-1c516b4250fe" - } - } + ] }, { "name": "io.github.kiwamizamurai/mcp-kibela-server", "description": "MCP server implementation for Kibela API integration", - "status": "active", "repository": { "url": "https://github.com/kiwamizamurai/mcp-kibela-server", "source": "github", @@ -9628,21 +7557,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "984ee65a-dec0-4a75-b21b-2efbcd9600bd", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "984ee65a-dec0-4a75-b21b-2efbcd9600bd" - } - } + ] }, { "name": "io.github.kong/mcp-konnect", "description": "A Model Context Protocol (MCP) server for interacting with Kong Konnect APIs, allowing AI assistants to query and analyze Kong Gateway configurations, traffic, and analytics.", - "status": "active", "repository": { "url": "https://github.com/Kong/mcp-konnect", "source": "github", @@ -9669,21 +7588,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "86a12728-3ab9-4a69-a447-bcb67957a5f7", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "86a12728-3ab9-4a69-a447-bcb67957a5f7" - } - } + ] }, { "name": "io.github.flux159/mcp-server-kubernetes", "description": "MCP Server for kubernetes management commands", - "status": "active", "repository": { "url": "https://github.com/Flux159/mcp-server-kubernetes", "source": "github", @@ -9700,21 +7609,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6a4d324e-7d12-495c-b72c-fe5803363af1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6a4d324e-7d12-495c-b72c-fe5803363af1" - } - } + ] }, { "name": "io.github.gongrzhe/langflow-doc-qa-server", "description": "A Model Context Protocol server for document Q&A powered by Langflow . It demonstrates core MCP concepts by providing a simple interface to query documents through a Langflow backend.", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/Langflow-DOC-QA-SERVER", "source": "github", @@ -9737,21 +7636,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3c265bda-03e4-4e46-bceb-437a1b02f431", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3c265bda-03e4-4e46-bceb-437a1b02f431" - } - } + ] }, { "name": "io.github.syucream/lightdash-mcp-server", "description": "A MCP(Model Context Protocol) server that accesses to Lightdash", - "status": "active", "repository": { "url": "https://github.com/syucream/lightdash-mcp-server", "source": "github", @@ -9778,21 +7667,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "17c2c7f2-20f2-424f-8466-b164ed19ddef", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "17c2c7f2-20f2-424f-8466-b164ed19ddef" - } - } + ] }, { "name": "io.github.tritlo/lsp-mcp", "description": "An MCP server that lets you interact with LSP servers", - "status": "active", "repository": { "url": "https://github.com/Tritlo/lsp-mcp", "source": "github", @@ -9821,21 +7700,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "32f876df-1f34-40d7-aed8-0431b32d5091", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "32f876df-1f34-40d7-aed8-0431b32d5091" - } - } + ] }, { "name": "io.github.tacticlaunch/mcp-linear", "description": "MCP server that enables AI assistants to interact with Linear project management system through natural language, allowing users to retrieve, create, and update issues, projects, and teams.", - "status": "active", "repository": { "url": "https://github.com/tacticlaunch/mcp-linear", "source": "github", @@ -9858,21 +7727,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a4a1013a-5789-4020-9a16-eda6c453aba7", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a4a1013a-5789-4020-9a16-eda6c453aba7" - } - } + ] }, { "name": "io.github.jerhadf/linear-mcp-server", "description": "A server that integrates Linear's project management system with the Model Context Protocol (MCP) to allow LLMs to interact with Linear.", - "status": "active", "repository": { "url": "https://github.com/jerhadf/linear-mcp-server", "source": "github", @@ -9895,21 +7754,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a0e6ce8a-15eb-4776-b9de-a4bb3c7e4730", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a0e6ce8a-15eb-4776-b9de-a4bb3c7e4730" - } - } + ] }, { "name": "io.github.run-llama/mcp-server-llamacloud", "description": "A MCP server connecting to managed indexes on LlamaCloud", - "status": "active", "repository": { "url": "https://github.com/run-llama/mcp-server-llamacloud", "source": "github", @@ -9978,21 +7827,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6cfb4c39-82bb-4b43-94a5-1f50ebd10c71", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6cfb4c39-82bb-4b43-94a5-1f50ebd10c71" - } - } + ] }, { "name": "io.github.cyberchitta/llm-context.py", "description": "Share code with LLMs via Model Context Protocol or clipboard. Rule-based customization enables easy switching between different tasks (like code review and documentation). Includes smart code outlining.", - "status": "active", "repository": { "url": "https://github.com/cyberchitta/llm-context.py", "source": "github", @@ -10021,21 +7860,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b9f04c6e-0d22-4f17-af83-ccde8b03e4e8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b9f04c6e-0d22-4f17-af83-ccde8b03e4e8" - } - } + ] }, { "name": "io.github.carterlasalle/mac_messages_mcp", "description": "An MCP server that securely interfaces with your iMessage database via the Model Context Protocol (MCP), allowing LLMs to query and analyze iMessage conversations. It includes robust phone number validation, attachment processing, contact management, group chat handling, and full support for sending and receiving messages.", - "status": "active", "repository": { "url": "https://github.com/carterlasalle/mac_messages_mcp", "source": "github", @@ -10052,21 +7881,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e7e51609-0d23-4d96-8063-d136bd993c93", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e7e51609-0d23-4d96-8063-d136bd993c93" - } - } + ] }, { "name": "io.github.abel9851/mcp-server-mariadb", "description": "An mcp server that provides read-only access to MariaDB.", - "status": "active", "repository": { "url": "https://github.com/abel9851/mcp-server-mariadb", "source": "github", @@ -10126,21 +7945,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "8197c60c-08c6-4d90-9d63-e64bf48ab6e4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "8197c60c-08c6-4d90-9d63-e64bf48ab6e4" - } - } + ] }, { "name": "io.github.liuyoshio/mcp-compass", "description": "MCP Discovery & Recommendation Service - Find the right MCP server for your needs", - "status": "active", "repository": { "url": "https://github.com/liuyoshio/mcp-compass", "source": "github", @@ -10157,21 +7966,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e4578522-e426-44f4-818c-68784f2a3038", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e4578522-e426-44f4-818c-68784f2a3038" - } - } + ] }, { "name": "io.github.tesla0225/mcp-create", "description": "", - "status": "active", "repository": { "url": "https://github.com/tesla0225/mcp-create", "source": "github", @@ -10188,21 +7987,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "84bda3c9-65ce-45f2-898f-6e370718b43d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "84bda3c9-65ce-45f2-898f-6e370718b43d" - } - } + ] }, { "name": "io.github.anaisbetts/mcp-installer", "description": "An MCP server that installs other MCP servers for you", - "status": "active", "repository": { "url": "https://github.com/anaisbetts/mcp-installer", "source": "github", @@ -10219,21 +8008,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9f5885d3-fe18-483d-b7b2-f507b2f37ccb", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9f5885d3-fe18-483d-b7b2-f507b2f37ccb" - } - } + ] }, { "name": "io.github.strowk/mcp-k8s-go", "description": "MCP server connecting to Kubernetes", - "status": "active", "repository": { "url": "https://github.com/strowk/mcp-k8s-go", "source": "github", @@ -10280,21 +8059,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a59a162f-d811-4344-bf53-606a74bb50c6", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a59a162f-d811-4344-bf53-606a74bb50c6" - } - } + ] }, { "name": "io.github.sparfenyuk/mcp-proxy", "description": "A bridge between Streamable HTTP and stdio MCP transports", - "status": "active", "repository": { "url": "https://github.com/sparfenyuk/mcp-proxy", "source": "github", @@ -10311,21 +8080,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5b01e888-7245-46a6-8747-d6f9ca374c43", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5b01e888-7245-46a6-8747-d6f9ca374c43" - } - } + ] }, { "name": "io.github.lciesielski/mcp-salesforce-example", "description": "", - "status": "active", "repository": { "url": "https://github.com/lciesielski/mcp-salesforce-example", "source": "github", @@ -10342,21 +8101,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3ec2249d-2ed3-45d2-8b52-185abfa918ac", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3ec2249d-2ed3-45d2-8b52-185abfa918ac" - } - } + ] }, { "name": "io.github.mem0ai/mem0-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/mem0ai/mem0-mcp", "source": "github", @@ -10373,21 +8122,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4f189cdd-04a5-47f6-b87b-7d38cc82e625", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4f189cdd-04a5-47f6-b87b-7d38cc82e625" - } - } + ] }, { "name": "io.github.ariadng/metatrader-mcp-server", "description": "Model Context Protocol (MCP) to enable AI LLMs to trade using MetaTrader platform", - "status": "active", "repository": { "url": "https://github.com/ariadng/metatrader-mcp-server", "source": "github", @@ -10436,21 +8175,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b3dbb84c-3e48-4cbc-a581-bb9ebe133cb4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b3dbb84c-3e48-4cbc-a581-bb9ebe133cb4" - } - } + ] }, { "name": "io.github.metricool/mcp-metricool", "description": "This is a Multi-Agent Collaboration Protocol (MCP) server for interacting with the Metricool API. It allows AI agents to access and analyze social media metrics and campaign data from your Metricool account.", - "status": "active", "repository": { "url": "https://github.com/metricool/mcp-metricool", "source": "github", @@ -10477,21 +8206,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "091ef217-8e6d-4e06-8cb9-bc5ffe5467b1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "091ef217-8e6d-4e06-8cb9-bc5ffe5467b1" - } - } + ] }, { "name": "io.github.jexinsam/mssql_mcp_server", "description": "A Model Context Protocol (MCP) server facilitating secure interactions with MSSQL databases.", - "status": "active", "repository": { "url": "https://github.com/JexinSam/mssql_mcp_server", "source": "github", @@ -10551,21 +8270,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "53290776-f4aa-488d-a10f-135a17645b02", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "53290776-f4aa-488d-a10f-135a17645b02" - } - } + ] }, { "name": "io.github.zcaceres/mcp-markdownify-server", "description": "A Model Context Protocol server for converting almost anything to Markdown", - "status": "active", "repository": { "url": "https://github.com/zcaceres/markdownify-mcp", "source": "github", @@ -10582,21 +8291,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4365781f-28ff-4865-aa7d-909678f6dbb4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4365781f-28ff-4865-aa7d-909678f6dbb4" - } - } + ] }, { "name": "io.github.jeff-nasseri/mikrotik-mcp", "description": "MCP server for Mikrotik", - "status": "active", "repository": { "url": "https://github.com/jeff-nasseri/mikrotik-mcp", "source": "github", @@ -10655,21 +8354,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5ebda83c-f2b3-4b14-ba72-13d940fa940a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5ebda83c-f2b3-4b14-ba72-13d940fa940a" - } - } + ] }, { "name": "io.github.yuchenssr/mindmap-mcp-server", "description": "mindmap, mcp server, artifact", - "status": "active", "repository": { "url": "https://github.com/YuChenSSR/mindmap-mcp-server", "source": "github", @@ -10707,21 +8396,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7f9416dd-34cf-4f0d-bcfb-c87cbee3abf6", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7f9416dd-34cf-4f0d-bcfb-c87cbee3abf6" - } - } + ] }, { "name": "io.github.mobile-next/mobile-mcp", "description": "Model Context Protocol Server for Mobile Automation and Scraping (iOS, Android, Emulators, Simulators and Physical Devices)", - "status": "active", "repository": { "url": "https://github.com/mobile-next/mobile-mcp", "source": "github", @@ -10738,21 +8417,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6fefc93f-3f03-47b0-9f42-21d5a5841490", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6fefc93f-3f03-47b0-9f42-21d5a5841490" - } - } + ] }, { "name": "io.github.kiliczsh/mcp-mongo-server", "description": "A Model Context Protocol Server for MongoDB", - "status": "active", "repository": { "url": "https://github.com/kiliczsh/mcp-mongo-server", "source": "github", @@ -10780,21 +8449,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b61a2d2e-db6c-47b4-9437-7edc8525d063", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b61a2d2e-db6c-47b4-9437-7edc8525d063" - } - } + ] }, { "name": "io.github.furey/mongodb-lens", - "description": "\ud83c\udf43\ud83d\udd0e MongoDB Lens: Full Featured MCP Server for MongoDB Databases", - "status": "active", + "description": "🍃🔎 MongoDB Lens: Full Featured MCP Server for MongoDB Databases", "repository": { "url": "https://github.com/furey/mongodb-lens", "source": "github", @@ -10811,21 +8470,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b4970e5b-1259-4ff9-a40a-c570f27d0930", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b4970e5b-1259-4ff9-a40a-c570f27d0930" - } - } + ] }, { "name": "io.github.sakce/mcp-server-monday", "description": "MCP Server to interact with Monday.com boards and items", - "status": "active", "repository": { "url": "https://github.com/sakce/mcp-server-monday", "source": "github", @@ -10842,21 +8491,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ac665c5c-e1de-4686-9012-5ac101186a31", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ac665c5c-e1de-4686-9012-5ac101186a31" - } - } + ] }, { "name": "io.github.yanmxa/multicluster-mcp-server", "description": "The gateway for GenAI systems to interact with multiple Kubernetes clusters through the MCP", - "status": "active", "repository": { "url": "https://github.com/yanmxa/multicluster-mcp-server", "source": "github", @@ -10873,21 +8512,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "55ba52cd-8e0a-4bcb-9a96-1658f2d713e0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "55ba52cd-8e0a-4bcb-9a96-1658f2d713e0" - } - } + ] }, { "name": "io.github.yuchenssr/multi-ai-advisor-mcp", "description": "council of models for decision", - "status": "active", "repository": { "url": "https://github.com/YuChenSSR/multi-ai-advisor-mcp", "source": "github", @@ -10904,21 +8533,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "abcce212-b3e1-4741-a8b3-86c95c276fb1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "abcce212-b3e1-4741-a8b3-86c95c276fb1" - } - } + ] }, { "name": "io.github.benborla/mcp-server-mysql", "description": "A Model Context Protocol server that provides read-only access to MySQL databases. This server enables LLMs to inspect database schemas and execute read-only queries.", - "status": "active", "repository": { "url": "https://github.com/benborla/mcp-server-mysql", "source": "github", @@ -10968,21 +8587,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "22fbb4c3-d5fd-4191-af7a-c78686cc6123", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "22fbb4c3-d5fd-4191-af7a-c78686cc6123" - } - } + ] }, { "name": "io.github.designcomputer/mysql_mcp_server", "description": "A Model Context Protocol (MCP) server that enables secure interaction with MySQL databases", - "status": "active", "repository": { "url": "https://github.com/designcomputer/mysql_mcp_server", "source": "github", @@ -11042,21 +8651,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "56577627-939e-4f31-8c96-654586538eb5", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "56577627-939e-4f31-8c96-654586538eb5" - } - } + ] }, { "name": "io.github.leonardsellem/n8n-mcp-server", "description": "MCP server that provides tools and resources for interacting with n8n API", - "status": "active", "repository": { "url": "https://github.com/leonardsellem/n8n-mcp-server", "source": "github", @@ -11094,21 +8693,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "309e2634-8163-47c9-951e-1cfec85e701b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "309e2634-8163-47c9-951e-1cfec85e701b" - } - } + ] }, { "name": "io.github.programcomputer/nasa-mcp-server", "description": "A Model Context Protocol (MCP) server for NASA APIs, providing a standardized interface for AI models to interact with NASA's vast array of data sources.", - "status": "active", "repository": { "url": "https://github.com/ProgramComputer/NASA-MCP-server", "source": "github", @@ -11187,21 +8776,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6a44b030-46c7-4390-adf2-2a75652dee27", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6a44b030-46c7-4390-adf2-2a75652dee27" - } - } + ] }, { "name": "io.github.stefanoamorelli/nasdaq-data-link-mcp", "description": "A Nasdaq Data Link MCP (Model Context Protocol) Server", - "status": "active", "repository": { "url": "https://github.com/stefanoamorelli/nasdaq-data-link-mcp", "source": "github", @@ -11230,21 +8809,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9326c18e-b2fb-4012-a170-cdd5753f0b1e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9326c18e-b2fb-4012-a170-cdd5753f0b1e" - } - } + ] }, { "name": "io.github.kyrietangsheng/mcp-server-nationalparks", "description": "", - "status": "active", "repository": { "url": "https://github.com/KyrieTangSheng/mcp-server-nationalparks", "source": "github", @@ -11267,21 +8836,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "010904ec-6e39-4bdc-878a-75a6e79d0500", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "010904ec-6e39-4bdc-878a-75a6e79d0500" - } - } + ] }, { "name": "io.github.pfldy2850/py-mcp-naver", "description": "python MCP NAVER", - "status": "active", "repository": { "url": "https://github.com/pfldy2850/py-mcp-naver", "source": "github", @@ -11298,21 +8857,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f10c234b-cf64-4dc2-9e38-5f57e53aeecb", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f10c234b-cf64-4dc2-9e38-5f57e53aeecb" - } - } + ] }, { "name": "io.github.taidgh-robinson/nba-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/Taidgh-Robinson/nba-mcp-server", "source": "github", @@ -11329,21 +8878,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9c7c309a-fb3c-4099-b461-0ca725c9083b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9c7c309a-fb3c-4099-b461-0ca725c9083b" - } - } + ] }, { "name": "io.github.r-huijts/ns-mcp-server", "description": "A Model Context Protocol (MCP) server that provides access to NS (Dutch Railways) travel information through Claude AI. This server enables Claude to fetch real-time train travel information and disruptions using the official Dutch NS API.", - "status": "active", "repository": { "url": "https://github.com/r-huijts/ns-mcp-server", "source": "github", @@ -11377,21 +8916,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "afcff9a2-a4ed-457f-80fb-8bdd1948771a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "afcff9a2-a4ed-457f-80fb-8bdd1948771a" - } - } + ] }, { "name": "io.github.da-okazaki/mcp-neo4j-server", "description": "mcp-neo4j-server", - "status": "active", "repository": { "url": "https://github.com/da-okazaki/mcp-neo4j-server", "source": "github", @@ -11422,21 +8951,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b4c38c39-7381-49fa-b7b4-75e2604ed93d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b4c38c39-7381-49fa-b7b4-75e2604ed93d" - } - } + ] }, { "name": "io.github.bigcodegen/mcp-neovim-server", "description": "Control Neovim using Model Context Protocol (MCP) and the official neovim/node-client JavaScript library", - "status": "active", "repository": { "url": "https://github.com/bigcodegen/mcp-neovim-server", "source": "github", @@ -11463,21 +8982,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d9d7e246-d3f4-4214-aa79-fe1981c73c2e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d9d7e246-d3f4-4214-aa79-fe1981c73c2e" - } - } + ] }, { "name": "io.github.kocierik/mcp-nomad", "description": " A nomad MCP Server (modelcontextprotocol) ", - "status": "active", "repository": { "url": "https://github.com/kocierik/mcp-nomad", "source": "github", @@ -11543,21 +9052,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6ebc1c16-ea8d-44eb-958c-74f3f0531058", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6ebc1c16-ea8d-44eb-958c-74f3f0531058" - } - } + ] }, { "name": "io.github.suekou/mcp-notion-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/suekou/mcp-notion-server", "source": "github", @@ -11595,21 +9094,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e9a5215c-6485-462f-b086-6e97f0466b30", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e9a5215c-6485-462f-b086-6e97f0466b30" - } - } + ] }, { "name": "io.github.v-3/notion-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/v-3/notion-server", "source": "github", @@ -11632,21 +9121,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1455eb68-3cf0-4cc3-a8ee-fd729a00acd3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1455eb68-3cf0-4cc3-a8ee-fd729a00acd3" - } - } + ] }, { "name": "io.github.teddyzxcv/ntfy-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/teddyzxcv/ntfy-mcp", "source": "github", @@ -11680,21 +9159,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "dd3f5299-5869-43ec-89a8-77e25e6a157e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "dd3f5299-5869-43ec-89a8-77e25e6a157e" - } - } + ] }, { "name": "io.github.gitmotion/ntfy-me-mcp", - "description": "An ntfy MCP server for sending/fetching ntfy notifications to any/self-hosted ntfy.sh server from AI Agents \ud83d\udce4 (supports secure token auth & more - use with npx or docker!)", - "status": "active", + "description": "An ntfy MCP server for sending/fetching ntfy notifications to any/self-hosted ntfy.sh server from AI Agents 📤 (supports secure token auth & more - use with npx or docker!)", "repository": { "url": "https://github.com/gitmotion/ntfy-me-mcp", "source": "github", @@ -11717,21 +9186,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ba368d1c-d64d-4ecd-9bde-72304d69c4d1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ba368d1c-d64d-4ecd-9bde-72304d69c4d1" - } - } + ] }, { "name": "io.github.calclavia/mcp-obsidian", "description": "A connector for Claude Desktop to read and search an Obsidian vault.", - "status": "active", "repository": { "url": "https://github.com/smithery-ai/mcp-obsidian", "source": "github", @@ -11759,21 +9218,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ab58cca1-94ad-42d8-be13-980556ac552b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ab58cca1-94ad-42d8-be13-980556ac552b" - } - } + ] }, { "name": "io.github.stevenstavrakis/obsidian-mcp", "description": "A simple MCP server for Obsidian", - "status": "active", "repository": { "url": "https://github.com/StevenStavrakis/obsidian-mcp", "source": "github", @@ -11810,21 +9259,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "752ef535-2865-4d95-bcc5-647a7be83f74", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "752ef535-2865-4d95-bcc5-647a7be83f74" - } - } + ] }, { "name": "io.github.gongrzhe/office-powerpoint-mcp-server", "description": "A MCP (Model Context Protocol) server for PowerPoint manipulation using python-pptx. This server provides tools for creating, editing, and manipulating PowerPoint presentations through the MCP protocol.", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/Office-PowerPoint-MCP-Server", "source": "github", @@ -11862,21 +9301,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d3194c4f-b73a-40b2-9170-0e6e8ee713c0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d3194c4f-b73a-40b2-9170-0e6e8ee713c0" - } - } + ] }, { "name": "io.github.gongrzhe/office-word-mcp-server", "description": "A Model Context Protocol (MCP) server for creating, reading, and manipulating Microsoft Word documents. This server enables AI assistants to work with Word documents through a standardized interface, providing rich document editing capabilities.", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/Office-Word-MCP-Server", "source": "github", @@ -11903,21 +9332,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4d3dba83-9775-4b5a-9de7-f18f7263460c", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4d3dba83-9775-4b5a-9de7-f18f7263460c" - } - } + ] }, { "name": "io.github.kapilduraphe/okta-mcp-server", "description": "Okta MCP Server", - "status": "active", "repository": { "url": "https://github.com/kapilduraphe/okta-mcp-server", "source": "github", @@ -11944,21 +9363,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "618671cb-8783-47c7-a81c-164f5f9e2cc1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "618671cb-8783-47c7-a81c-164f5f9e2cc1" - } - } + ] }, { "name": "io.github.conechoai/openai-websearch-mcp", "description": "openai websearch tool as mcp server", - "status": "active", "repository": { "url": "https://github.com/ConechoAI/openai-websearch-mcp", "source": "github", @@ -11993,21 +9402,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "531956a8-df6a-4edd-bb72-36c284c1d7c8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "531956a8-df6a-4edd-bb72-36c284c1d7c8" - } - } + ] }, { "name": "io.github.snaggle-ai/openapi-mcp-server", "description": "Allow AI to wade through complex OpenAPIs using Simple Language", - "status": "active", "repository": { "url": "https://github.com/janwilmake/openapi-mcp-server", "source": "github", @@ -12024,21 +9423,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "47685d63-d574-4311-b823-c0ddc29d8514", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "47685d63-d574-4311-b823-c0ddc29d8514" - } - } + ] }, { "name": "io.github.baryhuang/mcp-server-any-openapi", "description": "A MCP server that enables Claude to discover and call any API endpoint through semantic search. Intelligently chunks OpenAPI specifications to handle large API documentation, with built-in request execution capabilities. Perfect for integrating private APIs with Claude Desktop.", - "status": "active", "repository": { "url": "https://github.com/baryhuang/mcp-server-any-openapi", "source": "github", @@ -12114,21 +9503,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c52271b3-e48e-4896-b5a2-2ff93189b339", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c52271b3-e48e-4896-b5a2-2ff93189b339" - } - } + ] }, { "name": "io.github.hannesj/mcp-openapi-schema", "description": "OpenAPI Schema Model Context Protocol Server", - "status": "active", "repository": { "url": "https://github.com/hannesj/mcp-openapi-schema", "source": "github", @@ -12156,21 +9535,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7d68a487-bc40-4783-a2eb-1c9107fc26a4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7d68a487-bc40-4783-a2eb-1c9107fc26a4" - } - } + ] }, { "name": "io.github.spathodea-network/opencti-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/Spathodea-Network/opencti-mcp", "source": "github", @@ -12208,21 +9577,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c01d9a66-7a37-4f66-870d-38829c0047ea", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c01d9a66-7a37-4f66-870d-38829c0047ea" - } - } + ] }, { "name": "io.github.asusevski/opendota-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/asusevski/opendota-mcp-server", "source": "github", @@ -12269,21 +9628,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a81b1d9e-c1a5-4848-a3f5-350ea2872ae3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a81b1d9e-c1a5-4848-a3f5-350ea2872ae3" - } - } + ] }, { "name": "io.github.shanejonas/openrpc-mpc-server", "description": "A Model Context Protocol (MCP) server that provides JSON-RPC functionality through OpenRPC.", - "status": "active", "repository": { "url": "https://github.com/shanejonas/openrpc-mpc-server", "source": "github", @@ -12300,21 +9649,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9c804dd3-4283-4a5b-a34e-6f1c7065b671", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9c804dd3-4283-4a5b-a34e-6f1c7065b671" - } - } + ] }, { "name": "io.github.open-strategy-partners/osp_marketing_tools", "description": "A Model Context Protocol (MCP) server that empowers LLMs to use some of Open Srategy Partners' core writing and product marketing techniques.", - "status": "active", "repository": { "url": "https://github.com/open-strategy-partners/osp_marketing_tools", "source": "github", @@ -12343,21 +9682,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0d8b287c-6023-4809-a5ba-963c5612efa2", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0d8b287c-6023-4809-a5ba-963c5612efa2" - } - } + ] }, { "name": "io.github.vortiago/mcp-outline", "description": "A Model Context Protocol (MCP) server enabling AI assistants to interact with Outline documentation services.", - "status": "active", "repository": { "url": "https://github.com/Vortiago/mcp-outline", "source": "github", @@ -12374,21 +9703,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "d6801d4f-2f27-4e67-8b70-10754b48ff0d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "d6801d4f-2f27-4e67-8b70-10754b48ff0d" - } - } + ] }, { "name": "io.github.kukapay/pancakeswap-poolspy-mcp", "description": "An MCP server that tracks newly created liquidity pools on Pancake Swap", - "status": "active", "repository": { "url": "https://github.com/kukapay/pancakeswap-poolspy-mcp", "source": "github", @@ -12432,21 +9751,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "925a5d1d-ab12-44bd-9297-a20bdde6ec68", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "925a5d1d-ab12-44bd-9297-a20bdde6ec68" - } - } + ] }, { "name": "io.github.vivekvells/mcp-pandoc", "description": "MCP server for document format conversion using pandoc.", - "status": "active", "repository": { "url": "https://github.com/vivekVells/mcp-pandoc", "source": "github", @@ -12463,21 +9772,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a0f6fd01-5eb2-4aca-b4a6-c67eb6e0c5da", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a0f6fd01-5eb2-4aca-b4a6-c67eb6e0c5da" - } - } + ] }, { "name": "io.github.sv/mcp-paradex-py", "description": "Connect AI agents to the Paradex trading platform. Retrieve market data, manage accounts, and execute trades seamlessly. Enhance your trading experience with automated tools and real-time insights.", - "status": "active", "repository": { "url": "https://github.com/sv/mcp-paradex-py", "source": "github", @@ -12535,21 +9834,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "038a3024-c319-4a07-bd7f-9cf699cfebf1", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "038a3024-c319-4a07-bd7f-9cf699cfebf1" - } - } + ] }, { "name": "io.github.hungryrobot1/mcp-pif", "description": "A MCP implementation of the personal intelligence framework (PIF)", - "status": "active", "repository": { "url": "https://github.com/hungryrobot1/MCP-PIF", "source": "github", @@ -12566,21 +9855,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "dd586d17-4a7e-4658-92df-8cf6b58bff4d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "dd586d17-4a7e-4658-92df-8cf6b58bff4d" - } - } + ] }, { "name": "io.github.sirmews/mcp-pinecone", "description": "Model Context Protocol server to allow for reading and writing from Pinecone. Rudimentary RAG", - "status": "active", "repository": { "url": "https://github.com/sirmews/mcp-pinecone", "source": "github", @@ -12619,21 +9898,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "23b81af0-3e03-47b8-a310-c9b07bebd547", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "23b81af0-3e03-47b8-a310-c9b07bebd547" - } - } + ] }, { "name": "io.github.felores/placid-mcp-server", "description": "Generate image and video creatives using Placid.app templates in MCP compatible hosts", - "status": "active", "repository": { "url": "https://github.com/felores/placid-mcp-server", "source": "github", @@ -12650,21 +9919,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "cfe44c73-8b61-48c9-9d7f-b28e98d23b5b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "cfe44c73-8b61-48c9-9d7f-b28e98d23b5b" - } - } + ] }, { "name": "io.github.kelvin6365/plane-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/kelvin6365/plane-mcp-server", "source": "github", @@ -12681,21 +9940,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "dd8073c5-c7c4-42c1-a0d7-cad8089de750", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "dd8073c5-c7c4-42c1-a0d7-cad8089de750" - } - } + ] }, { "name": "io.github.executeautomation/mcp-playwright", - "description": "Playwright Model Context Protocol Server - Tool to automate Browsers and APIs in Claude Desktop, Cline, Cursor IDE and More \ud83d\udd0c", - "status": "active", + "description": "Playwright Model Context Protocol Server - Tool to automate Browsers and APIs in Claude Desktop, Cline, Cursor IDE and More 🔌", "repository": { "url": "https://github.com/executeautomation/mcp-playwright", "source": "github", @@ -12712,21 +9961,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "328e3069-005e-4179-add1-c13b18db4102", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "328e3069-005e-4179-add1-c13b18db4102" - } - } + ] }, { "name": "io.github.shannonlal/mcp-postman", "description": "MCP Server for running Postman Collections with Newman", - "status": "active", "repository": { "url": "https://github.com/shannonlal/mcp-postman", "source": "github", @@ -12743,21 +9982,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "503a5c73-c147-4b2c-90d3-292378268706", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "503a5c73-c147-4b2c-90d3-292378268706" - } - } + ] }, { "name": "io.github.allen-munsch/mcp-prefect", "description": "https://pypi.org/project/mcp-prefect/0.1.0/", - "status": "active", "repository": { "url": "https://github.com/allen-munsch/mcp-prefect", "source": "github", @@ -12774,21 +10003,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "94872987-6992-4fec-ab4b-9ef5311b3108", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "94872987-6992-4fec-ab4b-9ef5311b3108" - } - } + ] }, { "name": "io.github.kenjihikmatullah/productboard-mcp", "description": "Integrate the Productboard API into agentic workflows via MCP", - "status": "active", "repository": { "url": "https://github.com/kenjihikmatullah/productboard-mcp", "source": "github", @@ -12811,21 +10030,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "00613acb-73e2-4f93-8b96-296df17316c8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "00613acb-73e2-4f93-8b96-296df17316c8" - } - } + ] }, { "name": "io.github.pab1it0/prometheus-mcp-server", "description": "A Model Context Protocol (MCP) server that enables AI assistants to query and analyze Prometheus metrics through standardized interfaces.", - "status": "active", "repository": { "url": "https://github.com/pab1it0/prometheus-mcp-server", "source": "github", @@ -12908,21 +10117,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "cf42caa8-50d1-489d-aa3c-3e8432efe9e2", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "cf42caa8-50d1-489d-aa3c-3e8432efe9e2" - } - } + ] }, { "name": "io.github.sssjiang/pubchem_mcp_server", "description": "", - "status": "active", "repository": { "url": "https://github.com/sssjiang/pubchem_mcp_server", "source": "github", @@ -12939,21 +10138,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "68966508-9094-4eb5-9588-ff7456d6c1ce", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "68966508-9094-4eb5-9588-ff7456d6c1ce" - } - } + ] }, { "name": "io.github.dogukanakkaya/pulumi-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/dogukanakkaya/pulumi-mcp-server", "source": "github", @@ -13027,21 +10216,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4f9043b6-e17a-494e-89d4-52c3f39999ac", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4f9043b6-e17a-494e-89d4-52c3f39999ac" - } - } + ] }, { "name": "io.github.djannot/puppeteer-vision-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/djannot/puppeteer-vision-mcp", "source": "github", @@ -13058,21 +10237,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7997496a-c366-4949-9ca7-6741bcfdf2a0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7997496a-c366-4949-9ca7-6741bcfdf2a0" - } - } + ] }, { "name": "io.github.ashiknesin/pushover-mcp", "description": "A MCP implementation for sending notifications via Pushover", - "status": "active", "repository": { "url": "https://github.com/AshikNesin/pushover-mcp", "source": "github", @@ -13120,21 +10289,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "61ffe82e-5288-4af8-a45f-9a0907c941de", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "61ffe82e-5288-4af8-a45f-9a0907c941de" - } - } + ] }, { "name": "io.github.gongrzhe/quickchart-mcp-server", "description": "A Model Context Protocol server for generating charts using QuickChart.io . It allows you to create various types of charts through MCP tools.", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/Quickchart-MCP-Server", "source": "github", @@ -13151,21 +10310,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "904d15ba-e312-4dec-88b6-f52c1077bc96", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "904d15ba-e312-4dec-88b6-f52c1077bc96" - } - } + ] }, { "name": "io.github.66julienmartin/mcp-server-qwen_max", "description": "MCP server for Qwen Max model", - "status": "active", "repository": { "url": "https://github.com/66julienmartin/MCP-server-Qwen_Max", "source": "github", @@ -13188,21 +10337,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "92c66a82-172a-45e6-92a4-ee6a053e95e6", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "92c66a82-172a-45e6-92a4-ee6a053e95e6" - } - } + ] }, { "name": "io.github.apify/mcp-server-rag-web-browser", "description": "A MCP Server for the RAG Web Browser Actor", - "status": "active", "repository": { "url": "https://github.com/apify/mcp-server-rag-web-browser", "source": "github", @@ -13219,21 +10358,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "abcd869f-0159-408c-8965-9dbcc8c2d001", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "abcd869f-0159-408c-8965-9dbcc8c2d001" - } - } + ] }, { "name": "io.github.hiromitsusasaki/raindrop-io-mcp-server", "description": "An integration that allows LLMs to interact with Raindrop.io bookmarks using the Model Context Protocol (MCP).", - "status": "active", "repository": { "url": "https://github.com/hiromitsusasaki/raindrop-io-mcp-server", "source": "github", @@ -13267,21 +10396,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ddfb0c36-c116-4333-a62e-c9447d699b6e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ddfb0c36-c116-4333-a62e-c9447d699b6e" - } - } + ] }, { "name": "io.github.dschuler36/reaper-mcp-server", "description": "An MCP Server for interacting with Reaper projects.", - "status": "active", "repository": { "url": "https://github.com/dschuler36/reaper-mcp-server", "source": "github", @@ -13298,21 +10417,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c214300b-5fcc-48fb-ad3a-bb2d14b0403d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c214300b-5fcc-48fb-ad3a-bb2d14b0403d" - } - } + ] }, { "name": "io.github.gongrzhe/redis-mcp-server", "description": "A Redis MCP server (pushed to https://github.com/modelcontextprotocol/servers/tree/main/src/redis) implementation for interacting with Redis databases. This server enables LLMs to interact with Redis key-value stores through a set of standardized tools.", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/REDIS-MCP-Server", "source": "github", @@ -13349,21 +10458,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "01129bff-3d65-4e3d-8e82-6f2f269f818c", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "01129bff-3d65-4e3d-8e82-6f2f269f818c" - } - } + ] }, { "name": "io.github.prajwalnayak7/mcp-server-redis", "description": "MCP server to interact with Redis Server, AWS Memory DB, etc for caching or other use-cases where in-memory and key-value based storage is appropriate", - "status": "active", "repository": { "url": "https://github.com/prajwalnayak7/mcp-server-redis", "source": "github", @@ -13380,21 +10479,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "8f234bf9-d65f-4406-bed6-f8217adee4c9", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "8f234bf9-d65f-4406-bed6-f8217adee4c9" - } - } + ] }, { "name": "io.github.skydeckai/mcp-server-rememberizer", "description": "An MCP Server to enable global access to Rememberizer", - "status": "active", "repository": { "url": "https://github.com/skydeckai/mcp-server-rememberizer", "source": "github", @@ -13417,21 +10506,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "b5ee338d-655b-4067-9e17-f1bfc442a1eb", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "b5ee338d-655b-4067-9e17-f1bfc442a1eb" - } - } + ] }, { "name": "io.github.deepfates/mcp-replicate", "description": "Model Context Protocol server for Replicate's API", - "status": "active", "repository": { "url": "https://github.com/deepfates/mcp-replicate", "source": "github", @@ -13454,21 +10533,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ab0a0d85-7031-4eb0-bd01-5db69fb8c667", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ab0a0d85-7031-4eb0-bd01-5db69fb8c667" - } - } + ] }, { "name": "io.github.xxxbrian/mcp-rquest", "description": "A MCP server providing realistic browser-like HTTP request capabilities with accurate TLS/JA3/JA4 fingerprints for bypassing anti-bot measures. It also supports converting PDF and HTML documents to Markdown for easier processing by LLMs.", - "status": "active", "repository": { "url": "https://github.com/xxxbrian/mcp-rquest", "source": "github", @@ -13485,21 +10554,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0c43f128-7589-4ce6-9382-5399ba24bebd", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0c43f128-7589-4ce6-9382-5399ba24bebd" - } - } + ] }, { "name": "io.github.r-huijts/rijksmuseum-mcp", "description": "Rijksmuseum MCP integration for artwork exploration and analysis", - "status": "active", "repository": { "url": "https://github.com/r-huijts/rijksmuseum-mcp", "source": "github", @@ -13522,21 +10581,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6a64a548-c36c-400f-b43c-4d977aa1e482", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6a64a548-c36c-400f-b43c-4d977aa1e482" - } - } + ] }, { "name": "io.github.jifrozen0110/mcp-riot", "description": "League of Legends MCP Server", - "status": "active", "repository": { "url": "https://github.com/jifrozen0110/mcp-riot", "source": "github", @@ -13553,21 +10602,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "87e2c1e7-4fdb-436f-b52c-af683f63fb1b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "87e2c1e7-4fdb-436f-b52c-af683f63fb1b" - } - } + ] }, { "name": "io.github.smn2gnt/mcp-salesforce", "description": "MCP Salesforce connector", - "status": "active", "repository": { "url": "https://github.com/smn2gnt/MCP-Salesforce", "source": "github", @@ -13584,21 +10623,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0e45cb5a-5b45-47f3-8b5e-12bdf88d176a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0e45cb5a-5b45-47f3-8b5e-12bdf88d176a" - } - } + ] }, { "name": "io.github.adityak74/mcp-scholarly", "description": "A MCP server to search for accurate academic articles.", - "status": "active", "repository": { "url": "https://github.com/adityak74/mcp-scholarly", "source": "github", @@ -13615,21 +10644,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f43b2576-74d3-4b79-b9d0-0c5816835369", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f43b2576-74d3-4b79-b9d0-0c5816835369" - } - } + ] }, { "name": "io.github.cyberchitta/scrapling-fetch-mcp", "description": "Helps AI assistants access text content from bot-protected websites. MCP server that fetches HTML/markdown from sites with anti-automation measures using Scrapling.", - "status": "active", "repository": { "url": "https://github.com/cyberchitta/scrapling-fetch-mcp", "source": "github", @@ -13646,21 +10665,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "874336ad-3dfa-467f-8356-3de7d8b2b93d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "874336ad-3dfa-467f-8356-3de7d8b2b93d" - } - } + ] }, { "name": "io.github.ihor-sokoliuk/mcp-searxng", "description": "MCP Server for SearXNG", - "status": "active", "repository": { "url": "https://github.com/ihor-sokoliuk/mcp-searxng", "source": "github", @@ -13715,21 +10724,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "02cdb980-1dcd-4345-a8d1-d3f3f522d04b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "02cdb980-1dcd-4345-a8d1-d3f3f522d04b" - } - } + ] }, { "name": "io.github.stefanoamorelli/sec-edgar-mcp", "description": "A Model Context Protocol (MCP) Server for SEC EDGAR", - "status": "active", "repository": { "url": "https://github.com/stefanoamorelli/sec-edgar-mcp", "source": "github", @@ -13746,21 +10745,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "8d526532-7125-4b3c-b180-901914ead390", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "8d526532-7125-4b3c-b180-901914ead390" - } - } + ] }, { "name": "io.github.geli2001/shopify-mcp", "description": "MCP server for Shopify api, usable on mcp clients such as Anthropic's Claude and Cursor IDE", - "status": "active", "repository": { "url": "https://github.com/GeLi2001/shopify-mcp", "source": "github", @@ -13799,21 +10788,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "fcde90d4-5b99-4262-8101-7a507bf05915", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "fcde90d4-5b99-4262-8101-7a507bf05915" - } - } + ] }, { "name": "io.github.dvcrn/mcp-server-siri-shortcuts", "description": "MCP for calling Siri Shorcuts from LLMs", - "status": "active", "repository": { "url": "https://github.com/dvcrn/mcp-server-siri-shortcuts", "source": "github", @@ -13830,21 +10809,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "fbef362d-ba4b-4708-8579-332d2a57582d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "fbef362d-ba4b-4708-8579-332d2a57582d" - } - } + ] }, { "name": "io.github.isaacwasserman/mcp-snowflake-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/isaacwasserman/mcp-snowflake-server", "source": "github", @@ -13892,21 +10861,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "61942e80-99c9-4856-90c8-5a11ac76ade5", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "61942e80-99c9-4856-90c8-5a11ac76ade5" - } - } + ] }, { "name": "io.github.szeider/mcp-solver", "description": "Model Context Protocol (MCP) server for constraint optimization and solving\"", - "status": "active", "repository": { "url": "https://github.com/szeider/mcp-solver", "source": "github", @@ -13923,21 +10882,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "523175e8-6767-4387-af85-a46f256a6390", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "523175e8-6767-4387-af85-a46f256a6390" - } - } + ] }, { "name": "io.github.yeonupark/mcp-soccer-data", "description": "An MCP server that provides real-time football data based on the SoccerDataAPI.", - "status": "active", "repository": { "url": "https://github.com/yeonupark/mcp-soccer-data", "source": "github", @@ -13954,21 +10903,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "387ef1e6-0e4c-43ea-9416-bf3b399a3c76", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "387ef1e6-0e4c-43ea-9416-bf3b399a3c76" - } - } + ] }, { "name": "io.github.sendaifun/solana-agent-kit", "description": "connect any ai agents to solana protocols", - "status": "active", "repository": { "url": "https://github.com/sendaifun/solana-agent-kit", "source": "github", @@ -13985,21 +10924,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f25f6cdb-0623-4a04-bac2-f1b26331b432", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f25f6cdb-0623-4a04-bac2-f1b26331b432" - } - } + ] }, { "name": "io.github.varunneal/spotify-mcp", "description": "MCP to connect your LLM with Spotify.", - "status": "active", "repository": { "url": "https://github.com/varunneal/spotify-mcp", "source": "github", @@ -14051,21 +10980,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "20c38c50-831e-478a-9f52-064d3ad8a6ce", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "20c38c50-831e-478a-9f52-064d3ad8a6ce" - } - } + ] }, { "name": "io.github.r-huijts/strava-mcp", "description": "A Model Context Protocol (MCP) server that connects to Strava API, providing tools to access Strava data through LLMs", - "status": "active", "repository": { "url": "https://github.com/r-huijts/strava-mcp", "source": "github", @@ -14082,21 +11001,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "09a2ecf8-1774-490e-8542-2b25a6445a58", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "09a2ecf8-1774-490e-8542-2b25a6445a58" - } - } + ] }, { "name": "io.github.atharvagupta2003/mcp-stripe", "description": "", - "status": "active", "repository": { "url": "https://github.com/atharvagupta2003/mcp-stripe", "source": "github", @@ -14125,21 +11034,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "e532ff07-269a-47fb-98ad-6315d7081ccb", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "e532ff07-269a-47fb-98ad-6315d7081ccb" - } - } + ] }, { "name": "io.github.sonnylazuardi/cursor-talk-to-figma-mcp", "description": "Cursor Talk To Figma MCP", - "status": "active", "repository": { "url": "https://github.com/sonnylazuardi/cursor-talk-to-figma-mcp", "source": "github", @@ -14156,21 +11055,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9826cb7a-bc6f-41d9-b9b7-f76f781c1eca", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9826cb7a-bc6f-41d9-b9b7-f76f781c1eca" - } - } + ] }, { "name": "io.github.laksh-star/mcp-server-tmdb", "description": "MCP Server with TMDB", - "status": "active", "repository": { "url": "https://github.com/Laksh-star/mcp-server-tmdb", "source": "github", @@ -14193,21 +11082,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ff2b0e88-9426-4aa1-bfb1-165cc810344f", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ff2b0e88-9426-4aa1-bfb1-165cc810344f" - } - } + ] }, { "name": "io.github.ramxx/mcp-tavily", "description": "An MCP server for Tavily's search API", - "status": "active", "repository": { "url": "https://github.com/RamXX/mcp-tavily", "source": "github", @@ -14242,21 +11121,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "bdeb1cca-49ce-476a-87a2-f27c30b086bd", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "bdeb1cca-49ce-476a-87a2-f27c30b086bd" - } - } + ] }, { "name": "io.github.arturborycki/mcp-teradata", "description": "MCP Server for Teradata database", - "status": "active", "repository": { "url": "https://github.com/arturborycki/mcp-teradata", "source": "github", @@ -14273,21 +11142,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "cb9e2dc2-e335-4271-8e8d-5aff43cd389b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "cb9e2dc2-e335-4271-8e8d-5aff43cd389b" - } - } + ] }, { "name": "io.github.gongrzhe/terminal-controller-mcp", "description": "A Model Context Protocol (MCP) server that enables secure terminal command execution, directory navigation, and file system operations through a standardized interface.", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/terminal-controller-mcp", "source": "github", @@ -14316,21 +11175,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6275eefc-3f97-4a30-85ca-9678de62bdcf", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6275eefc-3f97-4a30-85ca-9678de62bdcf" - } - } + ] }, { "name": "io.github.geli2001/tft-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/GeLi2001/tft-mcp-server", "source": "github", @@ -14379,21 +11228,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1aa1e801-80d9-4ea1-80c2-e7d037ce98e3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1aa1e801-80d9-4ea1-80c2-e7d037ce98e3" - } - } + ] }, { "name": "io.github.kukapay/thegraph-mcp", "description": "An MCP server that powers AI agents with indexed blockchain data from The Graph.", - "status": "active", "repository": { "url": "https://github.com/kukapay/thegraph-mcp", "source": "github", @@ -14437,21 +11276,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ea41dc54-96fd-41db-90ec-92a1a45e7151", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ea41dc54-96fd-41db-90ec-92a1a45e7151" - } - } + ] }, { "name": "io.github.delorenj/mcp-server-ticketmaster", "description": "A Ticketmaster MCP server that provides query capabilites from the Discovery API", - "status": "active", "repository": { "url": "https://github.com/delorenj/mcp-server-ticketmaster", "source": "github", @@ -14474,21 +11303,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7f7268e0-670a-4630-83da-acc1090264c3", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7f7268e0-670a-4630-83da-acc1090264c3" - } - } + ] }, { "name": "io.github.alexarevalo9/ticktick-mcp-server", "description": "A Model Context Protocol (MCP) server designed to integrate with the TickTick task management platform, enabling intelligent context-aware task operations and automation.", - "status": "active", "repository": { "url": "https://github.com/alexarevalo9/ticktick-mcp-server", "source": "github", @@ -14580,21 +11399,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5c1709d9-3f57-4dbd-a727-5ce383a59c32", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5c1709d9-3f57-4dbd-a727-5ce383a59c32" - } - } + ] }, { "name": "io.github.abhiz123/todoist-mcp-server", "description": "MCP server for Todoist integration enabling natural language task management with Claude", - "status": "active", "repository": { "url": "https://github.com/abhiz123/todoist-mcp-server", "source": "github", @@ -14617,21 +11426,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "f79220af-20c6-4d21-b4f6-13f176b7f1d8", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "f79220af-20c6-4d21-b4f6-13f176b7f1d8" - } - } + ] }, { "name": "io.github.kukapay/token-minter-mcp", "description": "An MCP server providing tools for AI agents to mint ERC-20 tokens across multiple blockchains.", - "status": "active", "repository": { "url": "https://github.com/kukapay/token-minter-mcp", "source": "github", @@ -14669,21 +11468,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "10bb9deb-9384-4e4d-935e-9d830015a43e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "10bb9deb-9384-4e4d-935e-9d830015a43e" - } - } + ] }, { "name": "io.github.kukapay/token-revoke-mcp", "description": "An MCP server for checking and revoking ERC-20 token allowances across multiple blockchains.", - "status": "active", "repository": { "url": "https://github.com/kukapay/token-revoke-mcp", "source": "github", @@ -14710,21 +11499,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4bd856fc-761e-43e3-9236-7b996b6b7ec7", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4bd856fc-761e-43e3-9236-7b996b6b7ec7" - } - } + ] }, { "name": "io.github.suhail-ak-s/mcp-typesense-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/suhail-ak-s/mcp-typesense-server", "source": "github", @@ -14783,21 +11562,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a038d159-c51e-49ef-aa02-c794e8a5cb94", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a038d159-c51e-49ef-aa02-c794e8a5cb94" - } - } + ] }, { "name": "io.github.gongrzhe/travel-planner-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/GongRzhe/TRAVEL-PLANNER-MCP-Server", "source": "github", @@ -14820,21 +11589,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "7d8aa739-c9c6-4afd-991b-cf425be475db", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "7d8aa739-c9c6-4afd-991b-cf425be475db" - } - } + ] }, { "name": "io.github.kukapay/uniswap-poolspy-mcp", "description": "An MCP server that tracks newly created liquidity pools on Uniswap across nine blockchain networks.", - "status": "active", "repository": { "url": "https://github.com/kukapay/uniswap-poolspy-mcp", "source": "github", @@ -14867,21 +11626,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1ff3b09d-5c19-4254-849f-34244dc6ee88", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1ff3b09d-5c19-4254-849f-34244dc6ee88" - } - } + ] }, { "name": "io.github.kukapay/uniswap-trader-mcp", "description": "An MCP server for AI agents to automate token swaps on Uniswap DEX across multiple blockchains.", - "status": "active", "repository": { "url": "https://github.com/kukapay/uniswap-trader-mcp", "source": "github", @@ -14919,21 +11668,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "1dd6d7a5-ba98-4fce-b4a4-d0541df84569", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "1dd6d7a5-ba98-4fce-b4a4-d0541df84569" - } - } + ] }, { "name": "io.github.ognis1205/mcp-server-unitycatalog", "description": "Unity Catalog AI Model Context Protocol Server", - "status": "active", "repository": { "url": "https://github.com/ognis1205/mcp-server-unitycatalog", "source": "github", @@ -15000,21 +11739,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "51d74701-2a57-476f-a5ed-75a93be58f42", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "51d74701-2a57-476f-a5ed-75a93be58f42" - } - } + ] }, { "name": "io.github.codergamester/mcp-unity", "description": "MCP Server to integrate Unity Editor game engine with different AI Model clients (e.g. Claude Desktop, Windsurf, Cursor)", - "status": "active", "repository": { "url": "https://github.com/CoderGamester/mcp-unity", "source": "github", @@ -15031,21 +11760,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3cccaee3-1b35-4a97-bc23-d1a8603aa366", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3cccaee3-1b35-4a97-bc23-d1a8603aa366" - } - } + ] }, { "name": "io.github.quazaai/unitymcpintegration", "description": "Enable AI Agents to Control Unity", - "status": "active", "repository": { "url": "https://github.com/quazaai/UnityMCPIntegration", "source": "github", @@ -15068,21 +11787,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "162d7f33-0f96-47d7-980b-361fb0ddc381", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "162d7f33-0f96-47d7-980b-361fb0ddc381" - } - } + ] }, { "name": "io.github.isaacwasserman/mcp-vegalite-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/isaacwasserman/mcp-vegalite-server", "source": "github", @@ -15099,21 +11808,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "aa301cee-889b-4606-b66b-fd9dc1e77817", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "aa301cee-889b-4606-b66b-fd9dc1e77817" - } - } + ] }, { "name": "io.github.burningion/video-editing-mcp", "description": "MCP Interface for Video Jungle", - "status": "active", "repository": { "url": "https://github.com/burningion/video-editing-mcp", "source": "github", @@ -15166,21 +11865,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "6b136dc5-ffcf-41b0-bf5f-e5dff5d47981", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "6b136dc5-ffcf-41b0-bf5f-e5dff5d47981" - } - } + ] }, { "name": "io.github.mfukushim/map-traveler-mcp", "description": "Virtual traveler library for MCP", - "status": "active", "repository": { "url": "https://github.com/mfukushim/map-traveler-mcp", "source": "github", @@ -15215,21 +11904,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "8b5bb45d-a025-4e7b-a185-dbb5e349af32", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "8b5bb45d-a025-4e7b-a185-dbb5e349af32" - } - } + ] }, { "name": "io.github.dinghuazhou/sample-mcp-server-tos", "description": "", - "status": "active", "repository": { "url": "https://github.com/dinghuazhou/sample-mcp-server-tos", "source": "github", @@ -15292,21 +11971,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5f14d558-f478-43b5-8dde-20c9997f196a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5f14d558-f478-43b5-8dde-20c9997f196a" - } - } + ] }, { "name": "io.github.kapilduraphe/webflow-mcp-server", "description": "Webflow MCP server", - "status": "active", "repository": { "url": "https://github.com/kapilduraphe/webflow-mcp-server", "source": "github", @@ -15340,21 +12009,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c7876b07-1fe3-4006-a104-93ef9f9e1669", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c7876b07-1fe3-4006-a104-93ef9f9e1669" - } - } + ] }, { "name": "io.github.bharathvaj-ganesan/whois-mcp", "description": "MCP Server for whois lookups.", - "status": "active", "repository": { "url": "https://github.com/bharathvaj-ganesan/whois-mcp", "source": "github", @@ -15371,21 +12030,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "5f5a497c-673d-4988-b86b-c3bcaaf1a317", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "5f5a497c-673d-4988-b86b-c3bcaaf1a317" - } - } + ] }, { "name": "io.github.zzaebok/mcp-wikidata", "description": "A server implementation for Wikidata API using the Model Context Protocol (MCP).", - "status": "active", "repository": { "url": "https://github.com/zzaebok/mcp-wikidata", "source": "github", @@ -15402,21 +12051,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "02b79b89-cce8-4401-aa1f-255b01587266", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "02b79b89-cce8-4401-aa1f-255b01587266" - } - } + ] }, { "name": "io.github.simonb97/win-cli-mcp-server", "description": "Model Context Protocol server for secure command-line interactions on Windows systems", - "status": "active", "repository": { "url": "https://github.com/SimonB97/win-cli-mcp-server", "source": "github", @@ -15433,21 +12072,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a8a08cbd-074f-49ba-8f98-3ef8583cc93b", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a8a08cbd-074f-49ba-8f98-3ef8583cc93b" - } - } + ] }, { "name": "io.github.anshumax/world_bank_mcp_server", "description": "An implementation of the Model Context Protocol for the World Bank open data API", - "status": "active", "repository": { "url": "https://github.com/anshumax/world_bank_mcp_server", "source": "github", @@ -15485,21 +12114,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "4e6fc5a4-eecb-46e4-a3ce-4310cbb8c3f4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "4e6fc5a4-eecb-46e4-a3ce-4310cbb8c3f4" - } - } + ] }, { "name": "io.github.enescinr/twitter-mcp", "description": "A Model Context Protocol server allows to interact with Twitter, enabling posting tweets and searching Twitter.", - "status": "active", "repository": { "url": "https://github.com/EnesCinr/twitter-mcp", "source": "github", @@ -15534,21 +12153,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "ccb9aa0c-bbf5-46e6-a5f0-a5b4c11d519a", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "ccb9aa0c-bbf5-46e6-a5f0-a5b4c11d519a" - } - } + ] }, { "name": "io.github.vidhupv/x-mcp", "description": "", - "status": "active", "repository": { "url": "https://github.com/vidhupv/x-mcp", "source": "github", @@ -15593,21 +12202,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a24e506b-b16b-412d-b806-fcf7e39b4549", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a24e506b-b16b-412d-b806-fcf7e39b4549" - } - } + ] }, { "name": "io.github.shenghaiwang/xcodebuild", "description": "MCP tool for building Xcode iOS workspace/project and feeding back error to LLMs.", - "status": "active", "repository": { "url": "https://github.com/ShenghaiWang/xcodebuild", "source": "github", @@ -15624,21 +12223,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "bce1c06f-6325-418a-91e2-0ff2f6cf8ffc", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "bce1c06f-6325-418a-91e2-0ff2f6cf8ffc" - } - } + ] }, { "name": "io.github.john-zhang-dev/xero-mcp", "description": "A Model Context Protocol server allows Clients to interact with Xero", - "status": "active", "repository": { "url": "https://github.com/john-zhang-dev/xero-mcp", "source": "github", @@ -15669,21 +12258,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "aeca0d6b-ed78-4634-a29a-6d08f71de2b6", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "aeca0d6b-ed78-4634-a29a-6d08f71de2b6" - } - } + ] }, { "name": "io.github.xgenerationlab/xiyan_mcp_server", "description": "A Model Context Protocol (MCP) server that enables natural language queries to databases", - "status": "active", "repository": { "url": "https://github.com/XGenerationLab/xiyan_mcp_server", "source": "github", @@ -15700,21 +12279,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "a33de229-5e89-468d-aa2a-570b0f6e3dfa", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "a33de229-5e89-468d-aa2a-570b0f6e3dfa" - } - } + ] }, { "name": "io.github.apeyroux/mcp-xmind", "description": "", - "status": "active", "repository": { "url": "https://github.com/apeyroux/mcp-xmind", "source": "github", @@ -15742,21 +12311,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "cd63d589-6f0e-4d2d-8c9c-b6857f48cc62", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "cd63d589-6f0e-4d2d-8c9c-b6857f48cc62" - } - } + ] }, { "name": "io.github.chuckbryan/ynabmcpserver", "description": "", - "status": "active", "repository": { "url": "https://github.com/ChuckBryan/ynabmcpserver", "source": "github", @@ -15773,21 +12332,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c859bceb-143a-43dc-84e6-8ad8f707fcb0", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c859bceb-143a-43dc-84e6-8ad8f707fcb0" - } - } + ] }, { "name": "io.github.zubeidhendricks/youtube-mcp-server", "description": "MCP Server for YouTube API, enabling video management, Shorts creation, and advanced analytics", - "status": "active", "repository": { "url": "https://github.com/ZubeidHendricks/youtube-mcp-server", "source": "github", @@ -15810,21 +12359,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "0278fd92-a009-4a47-a6df-5f77db37e6ab", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "0278fd92-a009-4a47-a6df-5f77db37e6ab" - } - } + ] }, { "name": "io.github.prathamesh0901/zoom-mcp-server", "description": "", - "status": "active", "repository": { "url": "https://github.com/Prathamesh0901/zoom-mcp-server", "source": "github", @@ -15855,21 +12394,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "be45349a-793d-44d9-b2c6-6d4a30785a18", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "be45349a-793d-44d9-b2c6-6d4a30785a18" - } - } + ] }, { "name": "io.github.isdaniel/mcp_weather_server", "description": "", - "status": "active", "repository": { "url": "https://github.com/isdaniel/mcp_weather_server", "source": "github", @@ -15886,21 +12415,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "17829ab3-ae45-4fdd-95dc-4bb82a75752d", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "17829ab3-ae45-4fdd-95dc-4bb82a75752d" - } - } + ] }, { "name": "io.github.zcaceres/easy-mcp", "description": "Absurdly easy Model Context Protocol Servers in Typescript", - "status": "active", "repository": { "url": "https://github.com/zcaceres/easy-mcp", "source": "github", @@ -15917,21 +12436,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c636fa7e-796e-40a3-bdfc-52d7f4b82db4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c636fa7e-796e-40a3-bdfc-52d7f4b82db4" - } - } + ] }, { "name": "io.github.tadata-org/fastapi_mcp", "description": "Expose your FastAPI endpoints as Model Context Protocol (MCP) tools, with Auth!", - "status": "active", "repository": { "url": "https://github.com/tadata-org/fastapi_mcp", "source": "github", @@ -15948,21 +12457,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9297bbe7-81bc-4a7c-97ba-fcaaf8c7a626", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9297bbe7-81bc-4a7c-97ba-fcaaf8c7a626" - } - } + ] }, { "name": "io.github.punkpeye/fastmcp", "description": "A TypeScript framework for building MCP servers.", - "status": "active", "repository": { "url": "https://github.com/punkpeye/fastmcp", "source": "github", @@ -15996,21 +12495,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "c978017d-b94a-4d0c-a46e-5395cb94a67e", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "c978017d-b94a-4d0c-a46e-5395cb94a67e" - } - } + ] }, { "name": "io.github.mcpdotdirect/template-mcp-server", "description": "Template to quickly set up your own MCP server ", - "status": "active", "repository": { "url": "https://github.com/mcpdotdirect/template-mcp-server", "source": "github", @@ -16044,21 +12533,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "816a9400-2e2b-4697-8fbb-2a22fd81de74", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "816a9400-2e2b-4697-8fbb-2a22fd81de74" - } - } + ] }, { "name": "io.github.marimo-team/codemirror-mcp", "description": "CodeMirror extension to hook up a Model Context Provider (MCP)", - "status": "active", "repository": { "url": "https://github.com/marimo-team/codemirror-mcp", "source": "github", @@ -16081,21 +12560,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "bdcb0c25-b41c-4e61-ad25-89e5ddb35dcc", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "bdcb0c25-b41c-4e61-ad25-89e5ddb35dcc" - } - } + ] }, { "name": "io.github.wong2/mcp-cli", "description": "A CLI inspector for the Model Context Protocol", - "status": "active", "repository": { "url": "https://github.com/wong2/mcp-cli", "source": "github", @@ -16112,21 +12581,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "29cf2977-4a42-44f0-8e62-f92ca8391412", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "29cf2977-4a42-44f0-8e62-f92ca8391412" - } - } + ] }, { "name": "io.github.pathintegral-institute/mcpm.sh", "description": "CLI MCP package manager & registry for all platforms and all clients. Search & configure MCP servers. Advanced Router & Profile features.", - "status": "active", "repository": { "url": "https://github.com/pathintegral-institute/mcpm.sh", "source": "github", @@ -16143,21 +12602,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "963aac9c-4999-4ce6-93c9-43d4b2de16bb", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "963aac9c-4999-4ce6-93c9-43d4b2de16bb" - } - } + ] }, { "name": "io.github.zueai/mcp-manager", "description": "simple web ui to manage mcp (model context protocol) servers in the claude app", - "status": "active", "repository": { "url": "https://github.com/zueai/mcp-manager", "source": "github", @@ -16174,21 +12623,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "3288499d-b820-45c9-8abf-60c050c5c741", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "3288499d-b820-45c9-8abf-60c050c5c741" - } - } + ] }, { "name": "io.github.jeamee/mcphub-desktop", "description": "Desktop APP for Discover and Install MCP Servers", - "status": "active", "repository": { "url": "https://github.com/Jeamee/MCPHub-Desktop", "source": "github", @@ -16205,21 +12644,11 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "9f38c975-9a2a-484f-8173-562153f515e4", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "9f38c975-9a2a-484f-8173-562153f515e4" - } - } + ] }, { "name": "io.github.chatmcp/mcp-directory", "description": "directory for Awesome MCP Servers", - "status": "active", "repository": { "url": "https://github.com/chatmcp/mcpso", "source": "github", @@ -16236,15 +12665,6 @@ "type": "stdio" } } - ], - "_meta": { - "io.modelcontextprotocol.registry/official": { - "serverId": "53bef572-b137-447a-bb2f-9fdd37926619", - "isLatest": true, - "publishedAt": "0001-01-01T00:00:00Z", - "updatedAt": "0001-01-01T00:00:00Z", - "versionId": "53bef572-b137-447a-bb2f-9fdd37926619" - } - } + ] } ] diff --git a/docs/guides/administration/admin-operations.md b/docs/guides/administration/admin-operations.md index 498ae89..c6a946b 100644 --- a/docs/guides/administration/admin-operations.md +++ b/docs/guides/administration/admin-operations.md @@ -23,14 +23,17 @@ Use this when you need to modify details of a specific version (e.g., fix descri ### Step 1: Download Specific Version ```bash -export SERVER_ID="" # This is the server ID (consistent across versions) -export VERSION="" # e.g., "1.0.0" (optional, defaults to latest) +export SERVER_NAME="" # e.g., "com.example/my-server" +export VERSION="" # e.g., "1.0.0" (optional, defaults to latest) + +# URL encode the server name (replace / with %2F) +ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g') # Get specific version -curl -s "https://registry.modelcontextprotocol.io/v0/servers/${SERVER_ID}?version=${VERSION}" > server.json +curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}" > server.json -# Or get latest version (omit version parameter) -curl -s "https://registry.modelcontextprotocol.io/v0/servers/${SERVER_ID}" > server.json +# Or get latest version (omit /versions/VERSION) +curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}" > server.json ``` ### Step 2: Make Changes @@ -41,7 +44,7 @@ Open `server.json` and edit the specific version details. You cannot change the ```bash # Update specific version -curl -X PUT "https://registry.modelcontextprotocol.io/v0/servers/${SERVER_ID}?version=${VERSION}" \ +curl -X PUT "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}" \ -H "Authorization: Bearer ${REGISTRY_TOKEN}" \ -H "Content-Type: application/json" \ -d "$(cat server.json)" @@ -55,36 +58,35 @@ Use this when you need to apply changes across all versions of a server (e.g., m ### Step 1: List All Versions ```bash -export SERVER_ID="" # This is the server ID (consistent across versions) -curl -s "https://registry.modelcontextprotocol.io/v0/servers/${SERVER_ID}/versions" > all_versions.json +export SERVER_NAME="" # e.g., "com.example/my-server" +ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g') + +curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions" > all_versions.json ``` -### Step 2: Extract Version IDs +### Step 2: Extract Versions ```bash -# Extract all version IDs from the server -jq -r '.servers[]._meta."io.modelcontextprotocol.registry/official".version_id' all_versions.json > version_ids.txt +# Extract all versions from the server +jq -r '.servers[].server.version' all_versions.json > versions.txt ``` ### Step 3: Apply Changes to All Versions ```bash -# For each version, download, modify, and update -while read VERSION_ID; do - echo "Processing version: $VERSION_ID" - curl -s "https://registry.modelcontextprotocol.io/v0/servers/${VERSION_ID}" > temp_version.json - +# For each version, apply changes using the edit endpoint +while read VERSION; do + echo "Processing version: $VERSION" + # Apply your changes here (e.g., set status to "deleted") - jq '.status = "deleted"' temp_version.json > modified_version.json - - curl -X PUT "https://registry.modelcontextprotocol.io/v0/servers/${VERSION_ID}" \ + curl -X PUT "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}?status=deleted" \ -H "Authorization: Bearer ${REGISTRY_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"server\": $(cat modified_version.json)}" -done < version_ids.txt + -H "Content-Type: application/json" + +done < versions.txt # Clean up temporary files -rm temp_version.json modified_version.json version_ids.txt all_versions.json +rm versions.txt all_versions.json ``` ## Quick Operations @@ -92,29 +94,47 @@ rm temp_version.json modified_version.json version_ids.txt all_versions.json ### Get Latest Version of a Server ```bash -export SERVER_ID="" -curl -s "https://registry.modelcontextprotocol.io/v0/servers/${SERVER_ID}" > latest_version.json -export VERSION_ID=$(jq -r '._meta."io.modelcontextprotocol.registry/official".version_id' latest_version.json) -echo "Latest version ID: $VERSION_ID" +export SERVER_NAME="" # e.g., "com.example/my-server" +ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g') + +curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}" > latest_version.json +export VERSION=$(jq -r '.server.version' latest_version.json) +echo "Latest version: $VERSION" ``` ### Takedown a Specific Version ```bash -export VERSION_ID="" -./tools/admin/takedown.sh +export SERVER_NAME="" # e.g., "com.example/my-server" +export VERSION="" # e.g., "1.0.0" +export REGISTRY_TOKEN="" + +REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" VERSION="$VERSION" ./tools/admin/takedown.sh ``` -### Takedown an Entire Server (All Versions) +### Takedown Latest Version (Entire Server) ```bash -export SERVER_ID="" -# Get all version IDs and takedown each one -curl -s "https://registry.modelcontextprotocol.io/v0/servers/${SERVER_ID}/versions" | \ - jq -r '.servers[]._meta."io.modelcontextprotocol.registry/official".version_id' | \ - while read VERSION_ID; do - echo "Taking down version: $VERSION_ID" - ./tools/admin/takedown.sh "$VERSION_ID" +export SERVER_NAME="" # e.g., "com.example/my-server" +export REGISTRY_TOKEN="" + +# This marks the latest version as deleted +REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" ./tools/admin/takedown.sh +``` + +### Takedown All Versions of a Server + +```bash +export SERVER_NAME="" # e.g., "com.example/my-server" +export REGISTRY_TOKEN="" +ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g') + +# Get all versions and takedown each one +curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions" | \ + jq -r '.servers[].server.version' | \ + while read VERSION; do + echo "Taking down version: $VERSION" + REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" VERSION="$VERSION" ./tools/admin/takedown.sh done ``` diff --git a/docs/guides/consuming/use-rest-api.md b/docs/guides/consuming/use-rest-api.md index fffc8ea..928b863 100644 --- a/docs/guides/consuming/use-rest-api.md +++ b/docs/guides/consuming/use-rest-api.md @@ -9,9 +9,9 @@ Integration patterns and best practices for building applications that consume M **Authentication**: Not required for read-only access - **`GET /v0/servers`** - List all servers with pagination -- **`GET /v0/servers/{server_id}`** - Get latest version of server by server ID -- **`GET /v0/servers/{server_id}?version=X.X.X`** - Get specific version of server -- **`GET /v0/servers/{server_id}/versions`** - List all versions of a server +- **`GET /v0/servers/{serverName}`** - Get latest version of server by server name (URL-encoded) +- **`GET /v0/servers/{serverName}/versions/{version}`** - Get specific version of server +- **`GET /v0/servers/{serverName}/versions`** - List all versions of a server See the [interactive API documentation](https://registry.modelcontextprotocol.io/docs) for complete request/response schemas. @@ -22,6 +22,32 @@ See the [interactive API documentation](https://registry.modelcontextprotocol.io For now we recommend scraping the `GET /v0/servers` endpoint on some regular basis. In the future we might provide a filter for updatedAt ([#291](https://github.com/modelcontextprotocol/registry/issues/291)) to get only recently changed servers. +### Pagination Example + +The API uses cursor-based pagination. Here's how to fetch all servers: + +```bash +# Initial request +curl "https://registry.modelcontextprotocol.io/v0/servers?limit=100" +``` + +```json +{ + "servers": [...], + "metadata": { + "count": 100, + "nextCursor": "com.example/my-server:1.0.0" + } +} +``` + +```bash +# Next page using cursor +curl "https://registry.modelcontextprotocol.io/v0/servers?limit=100&cursor=com.example%2Fmy-server%3A1.0.0" +``` + +**Important**: Always URL-encode cursor values when using them in query parameters. + Servers are generally immutable, except for the `status` field which can be updated to `deleted` (among other states). For these packages, we recommend you also update the status field to `deleted` or remove the package from your registry quickly. This is because this status generally indicates it has violated our permissive [moderation guidelines](../administration/moderation-guidelines.md), suggesting it is illegal, malware or spam. ### Filtering & Enhancement @@ -32,7 +58,7 @@ You can also add custom metadata to servers using the `_meta` field. For example ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.yourname/weather-server", "description": "MCP server for weather data access", "status": "active", diff --git a/docs/guides/publishing/publish-server.md b/docs/guides/publishing/publish-server.md index 93872d8..95c50ee 100644 --- a/docs/guides/publishing/publish-server.md +++ b/docs/guides/publishing/publish-server.md @@ -92,7 +92,7 @@ This creates a `server.json` with auto-detected values. You'll see something lik ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.yourname/your-server", "description": "A description of your MCP server", "version": "1.0.0", @@ -151,7 +151,7 @@ Add an `mcpName` field to your `package.json`: ### Example server.json ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.username/server-name", "description": "A server that provides npm package functionality", "version": "1.0.0", @@ -189,7 +189,7 @@ Add it to your README.md file (which becomes the package description on PyPI). T ### Example server.json ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.username/server-name", "description": "A server that provides PyPI package functionality", "version": "1.0.0", @@ -227,7 +227,7 @@ Add a README file to your NuGet package that includes the server name. This can ### Example server.json ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.username/server-name", "description": "A server that provides NuGet package functionality", "version": "1.0.0", @@ -269,7 +269,7 @@ LABEL io.modelcontextprotocol.server.name="io.github.username/server-name" ### Example server.json (Docker Hub) ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.username/server-name", "description": "A server that provides Docker container functionality", "version": "1.0.0", @@ -290,7 +290,7 @@ LABEL io.modelcontextprotocol.server.name="io.github.username/server-name" ### Example server.json (GitHub Container Registry) ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.username/server-name", "description": "A server that provides GitHub container functionality", "version": "1.0.0", @@ -332,7 +332,7 @@ openssl dgst -sha256 server.mcpb ### Example server.json ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.username/server-name", "description": "A server that provides MCPB package functionality", "version": "1.0.0", @@ -377,7 +377,7 @@ Add the `remotes` field to your `server.json` (can coexist with `packages`): ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "com.yourcompany/api-server", "description": "Cloud-hosted MCP server for API operations", "version": "2.0.0", diff --git a/docs/reference/api/CHANGELOG.md b/docs/reference/api/CHANGELOG.md index 9c618b9..96fbb17 100644 --- a/docs/reference/api/CHANGELOG.md +++ b/docs/reference/api/CHANGELOG.md @@ -2,6 +2,30 @@ Changes to the REST API endpoints and responses. +## 2025-09-29 + +### ⚠️ BREAKING CHANGES + +#### API Endpoint Restructuring + +API endpoints updated to use server names instead of server IDs for better usability. + +**Changed endpoints:** +- `GET /v0/servers/{server_id}` → `GET /v0/servers/{serverName}` +- `GET /v0/servers/{server_id}/versions` → `GET /v0/servers/{serverName}/versions` + +**New endpoints:** +- `GET /v0/servers/{serverName}/versions/{version}` - Get specific server version +- `PUT /v0/servers/{serverName}/versions/{version}` - Edit server version (admin only) + +**Response format changes:** +- Introduced `ServerResponse` schema separating server data from registry metadata +- Moved `status` field from server data to `_meta.io.modelcontextprotocol.registry/official` +- Removed `io.modelcontextprotocol.registry/official` metadata from `ServerDetail` schema + +### Changed +- OpenAPI spec version: `2025-09-16` → `2025-09-29` + ## 2025-09-16 ### ⚠️ BREAKING CHANGES diff --git a/docs/reference/api/generic-registry-api.md b/docs/reference/api/generic-registry-api.md index f128bb9..8190017 100644 --- a/docs/reference/api/generic-registry-api.md +++ b/docs/reference/api/generic-registry-api.md @@ -15,9 +15,9 @@ The official registry has some more endpoints and restrictions on top of this. S ### Core Endpoints - **`GET /v0/servers`** - List all servers with pagination -- **`GET /v0/servers/{server_id}`** - Get latest version of server by server ID -- **`GET /v0/servers/{server_id}?version=X.X.X`** - Get specific version of server -- **`GET /v0/servers/{server_id}/versions`** - List all versions of a server +- **`GET /v0/servers/{serverName}`** - Get latest version of server by server name (URL-encoded) +- **`GET /v0/servers/{serverName}/versions/{version}`** - Get specific version of server (both parameters should be URL-encoded) +- **`GET /v0/servers/{serverName}/versions`** - List all versions of a server - **`POST /v0/publish`** - Publish new server (optional, registry-specific authentication) ### Authentication @@ -27,6 +27,16 @@ The official registry has some more endpoints and restrictions on top of this. S ### Content Type All requests and responses use `application/json` +### Pagination +List endpoints use cursor-based pagination for efficient, stable results. + +#### Usage +1. **Initial request**: Omit the `cursor` parameter +2. **Subsequent requests**: Use the `nextCursor` value from the previous response +3. **End of results**: When `nextCursor` is null or empty, there are no more results + +**Important**: Always treat cursors as opaque strings. Never manually construct or modify cursor values. + ### Basic Example: List Servers ```bash @@ -37,15 +47,23 @@ curl https://registry.example.com/v0/servers?limit=10 { "servers": [ { - "name": "io.modelcontextprotocol/filesystem", - "description": "Filesystem operations server", - "status": "active", - "version": "1.0.2" + "server": { + "name": "io.modelcontextprotocol/filesystem", + "description": "Filesystem operations server", + "version": "1.0.2" + }, + "_meta": { + "io.modelcontextprotocol.registry/official": { + "status": "active", + "publishedAt": "2025-01-01T10:30:00Z", + "isLatest": true + } + } } ], "metadata": { "count": 10, - "next_cursor": "eyJ..." + "nextCursor": "com.example/my-server:1.0.0" } } ``` diff --git a/docs/reference/api/official-registry-api.md b/docs/reference/api/official-registry-api.md index 4bacc9f..491b6c5 100644 --- a/docs/reference/api/official-registry-api.md +++ b/docs/reference/api/official-registry-api.md @@ -58,4 +58,4 @@ Example: `GET /v0/servers?search=filesystem&updated_since=2025-08-01T00:00:00Z&v #### Admin endpoints - GET `/metrics` - Prometheus metrics endpoint - GET `/v0/health` - Basic health check endpoint -- PUT `/v0/servers/{server_id}?version=X.X.X` - Edit specific server version (version parameter required) +- PUT `/v0/servers/{serverName}/versions/{version}` - Edit specific server version diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index ab1fd49..19fd332 100644 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -1,9 +1,9 @@ openapi: 3.1.0 jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema" -$id: https://modelcontextprotocol.io/schemas/draft/2025-09-16/server-registry-openapi +$id: https://modelcontextprotocol.io/schemas/draft/2025-09-29/server-registry-openapi info: title: MCP Server Registry API - version: "2025-09-16" + version: "2025-09-29" summary: API for discovering and accessing MCP server metadata description: | Specification for a theoretical REST API that serves up metadata about MCP servers. @@ -19,7 +19,10 @@ paths: parameters: - name: cursor in: query - description: Pagination cursor for retrieving next set of results + description: | + Pagination cursor for retrieving next set of results. + + Cursors are opaque strings returned in the `metadata.nextCursor` field of paginated responses. Always use the exact cursor value returned by the API. required: false schema: type: string @@ -36,30 +39,25 @@ paths: application/json: schema: $ref: '#/components/schemas/ServerList' - /v0/servers/{server_id}: + /v0/servers/{serverName}: get: summary: Get MCP server details - description: Returns detailed information about a specific MCP server. Returns latest version by default, or specific version if version query parameter is provided. + description: Returns detailed information about the latest version of a specific MCP server. parameters: - - name: server_id + - name: serverName in: path required: true - description: Unique server ID (consistent across all versions) - schema: - type: string - format: uuid - - name: version - in: query - description: Specific version to retrieve (e.g., "1.0.0"). If not provided, returns latest version. + description: URL-encoded server name (e.g., "com.example%2Fmy-server") schema: type: string + example: "com.example%2Fmy-server" responses: '200': description: Detailed server information content: application/json: schema: - $ref: '#/components/schemas/ServerDetail' + $ref: '#/components/schemas/ServerResponse' '404': description: Server not found content: @@ -70,18 +68,18 @@ paths: error: type: string example: "Server not found" - /v0/servers/{server_id}/versions: + /v0/servers/{serverName}/versions: get: summary: List all versions of an MCP server description: Returns all available versions for a specific MCP server, ordered by publication date (newest first) parameters: - - name: server_id + - name: serverName in: path required: true - description: Unique server ID (consistent across all versions) + description: URL-encoded server name (e.g., "com.example%2Fmy-server") schema: type: string - format: uuid + example: "com.example%2Fmy-server" responses: '200': description: A list of all versions for the server @@ -99,6 +97,42 @@ paths: error: type: string example: "Server not found" + /v0/servers/{serverName}/versions/{version}: + get: + summary: Get specific MCP server version + description: Returns detailed information about a specific version of an MCP server. + parameters: + - name: serverName + in: path + required: true + description: URL-encoded server name (e.g., "com.example%2Fmy-server") + schema: + type: string + example: "com.example%2Fmy-server" + - name: version + in: path + required: true + description: URL-encoded version to retrieve (e.g., "1.0.0" or "1.0.0%2B20130313144700" for versions with build metadata) + schema: + type: string + example: "1.0.0" + responses: + '200': + description: Detailed server information + content: + application/json: + schema: + $ref: '#/components/schemas/ServerResponse' + '404': + description: Server or version not found + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Server not found" /v0/publish: post: summary: Publish MCP server (Optional) @@ -122,7 +156,6 @@ paths: value: name: "io.modelcontextprotocol/filesystem" description: "Node.js server implementing Model Context Protocol (MCP) for filesystem operations." - status: "active" repository: url: "https://github.com/modelcontextprotocol/servers" source: "github" @@ -136,7 +169,7 @@ paths: io.modelcontextprotocol.registry/publisher-provided: tool: "publisher-cli" version: "1.2.3" - build_info: + buildInfo: commit: "abc123def456" timestamp: "2023-12-01T10:30:00Z" example_org_server: @@ -144,7 +177,6 @@ paths: value: name: "com.example/demo-server" description: "Example MCP server demonstrating publisher extensions." - status: "active" repository: url: "https://github.com/example/mcp-demo" source: "github" @@ -156,22 +188,22 @@ paths: version: "1.0.0" _meta: io.modelcontextprotocol.registry/publisher-provided: - contact_email: "maintainer@example.com" - build_metadata: + contactEmail: "maintainer@example.com" + buildMetadata: commit: "abc123" timestamp: "2024-01-15T10:30:00Z" com.example: - marketplace_icon: "https://example.com/icon.png" + marketplaceIcon: "https://example.com/icon.png" category: ["software_development"] - documentation_url: "https://docs.example.org" - verified_publisher: true + documentationUrl: "https://docs.example.org" + verifiedPublisher: true responses: '200': description: Successfully published server content: application/json: schema: - $ref: '#/components/schemas/ServerDetail' + $ref: '#/components/schemas/ServerResponse' '401': description: Unauthorized - Invalid or missing authentication token content: @@ -250,12 +282,6 @@ components: type: string description: "Human-readable description of the server's functionality" example: "Node.js server implementing Model Context Protocol (MCP) for filesystem operations." - status: - type: string - enum: [active, deprecated] - default: active - description: "Server lifecycle status. 'deprecated' indicates the server is no longer recommended for new usage." - example: "active" repository: $ref: '#/components/schemas/Repository' version: @@ -277,13 +303,15 @@ components: servers: type: array items: - $ref: '#/components/schemas/ServerDetail' + $ref: '#/components/schemas/ServerResponse' metadata: type: object properties: - next_cursor: + nextCursor: type: string - description: Cursor for next page of results + description: | + Pagination cursor for retrieving the next page of results. + Use this exact value in the `cursor` query parameter of your next request. If null or empty, there are no more results. count: type: integer description: Number of items in current page @@ -491,7 +519,7 @@ components: type: string format: uri description: JSON Schema URI for this server.json format - example: "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json" + example: "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json" packages: type: array items: @@ -511,43 +539,46 @@ components: example: tool: "publisher-cli" version: "1.2.3" - build_info: + buildInfo: commit: "abc123def456" timestamp: "2023-12-01T10:30:00Z" - pipeline_id: "build-789" - io.modelcontextprotocol.registry/official: - type: object - description: Registry-specific metadata managed by the MCP registry system - required: - - serverId - - versionId - - publishedAt - - updatedAt - - isLatest - properties: - serverId: - type: string - format: uuid - description: Consistent ID across all versions of a server - example: "550e8400-e29b-41d4-a716-446655440000" - versionId: - type: string - format: uuid - description: Unique ID for this specific version - example: "773f9b2e-1a47-4c8d-b5e6-2f8d9c4a7b3e" - publishedAt: - type: string - format: date-time - description: Timestamp when the server was first published to the registry - example: "2023-12-01T10:30:00Z" - updatedAt: - type: string - format: date-time - description: Timestamp when the server entry was last updated - example: "2023-12-01T11:00:00Z" - isLatest: - type: boolean - description: Whether this is the latest version of the server - example: true - additionalProperties: false - additionalProperties: true + pipelineId: "build-789" + + ServerResponse: + description: API response format with separated server data and registry metadata + type: object + required: + - server + - _meta + properties: + server: + $ref: '#/components/schemas/ServerDetail' + _meta: + type: object + description: Registry-managed metadata + properties: + io.modelcontextprotocol.registry/official: + type: object + description: Official MCP registry metadata + properties: + status: + type: string + enum: ["active", "deprecated", "deleted"] + description: Server lifecycle status + example: "active" + publishedAt: + type: string + format: date-time + description: Timestamp when the server was first published to the registry + example: "2023-12-01T10:30:00Z" + updatedAt: + type: string + format: date-time + description: Timestamp when the server entry was last updated + example: "2023-12-01T11:00:00Z" + isLatest: + type: boolean + description: Whether this is the latest version of the server + example: true + additionalProperties: false + additionalProperties: true diff --git a/docs/reference/server-json/CHANGELOG.md b/docs/reference/server-json/CHANGELOG.md index 912c349..f828ee8 100644 --- a/docs/reference/server-json/CHANGELOG.md +++ b/docs/reference/server-json/CHANGELOG.md @@ -2,6 +2,24 @@ Changes to the server.json schema and format. +## 2025-09-29 + +### ⚠️ BREAKING CHANGES + +#### Schema Simplification + +Removed registry-managed fields from publisher-controlled server.json schema. + +**Removed fields:** +- `status` field from Server object (now managed by registry in API responses) +- `io.modelcontextprotocol.registry/official` from `_meta` (read-only, added by registry) + +**Migration:** +Publishers should remove these fields from their `server.json` files. The registry will manage server status and official metadata separately. + +### Changed +- Schema version: `2025-09-16` → `2025-09-29` + ## 2025-09-16 ### ⚠️ BREAKING CHANGES @@ -116,7 +134,7 @@ All JSON field names standardized to camelCase. **All existing `server.json` fil #### Updated Schema Reference -🔗 **Current schema**: https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json +🔗 **Current schema**: https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json ### Changed - Schema version: `2025-07-09` → `2025-09-16` diff --git a/docs/reference/server-json/generic-server-json.md b/docs/reference/server-json/generic-server-json.md index b34e7b9..7da206f 100644 --- a/docs/reference/server-json/generic-server-json.md +++ b/docs/reference/server-json/generic-server-json.md @@ -8,7 +8,7 @@ Also see: ## Browse the Complete Schema -**📋 View the full specification interactively**: Open [server.schema.json](./server.schema.json) in a schema viewer like [json-schema.app](https://json-schema.app/view/%23?url=https%3A%2F%2Fstatic.modelcontextprotocol.io%2Fschemas%2F2025-09-16%2Fserver.schema.json). +**📋 View the full specification interactively**: Open [server.schema.json](./server.schema.json) in a schema viewer like [json-schema.app](https://json-schema.app/view/%23?url=https%3A%2F%2Fstatic.modelcontextprotocol.io%2Fschemas%2F2025-09-29%2Fserver.schema.json). The schema contains all field definitions, validation rules, examples, and detailed descriptions. @@ -22,7 +22,7 @@ The official registry has some more restrictions on top of this. See the [offici ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol.anonymous/brave-search", "description": "MCP server for Brave Search API integration", "status": "active", @@ -69,7 +69,7 @@ For MCP servers located within a subdirectory of a larger repository (monorepo s ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol/everything", "description": "MCP server that exercises all the features of the MCP protocol", "status": "active", @@ -151,7 +151,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.modelcontextprotocol/filesystem", "description": "Node.js server implementing Model Context Protocol (MCP) for filesystem operations.", "status": "active", @@ -498,7 +498,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6. ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol.anonymous/hybrid-mcp", "description": "MCP server available as both local package and remote service", "repository": { @@ -626,7 +626,7 @@ Some CLI tools bundle an MCP server, without a standalone MCP package or a publi ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.snyk/cli-mcp", "description": "MCP server provided by the Snyk CLI", "status": "active", @@ -661,7 +661,7 @@ For MCP servers that follow a custom installation path or are embedded in applic ```json { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol.anonymous/embedded-mcp", "description": "MCP server embedded in a Desktop app", "status": "active", diff --git a/docs/reference/server-json/server.schema.json b/docs/reference/server-json/server.schema.json index 8766b2a..3d367c9 100644 --- a/docs/reference/server-json/server.schema.json +++ b/docs/reference/server-json/server.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "$id": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "title": "MCP Server Detail", "$ref": "#/definitions/ServerDetail", "definitions": { @@ -58,12 +58,6 @@ "minLength": 1, "maxLength": 100 }, - "status": { - "type": "string", - "enum": ["active", "deprecated", "deleted"], - "default": "active", - "description": "Server lifecycle status. 'deprecated' indicates the server is no longer recommended for new usage. 'deleted' indicates the server should never be installed and existing installations should be uninstalled - this is rare, and usually indicates malware or a legal takedown." - }, "repository": { "$ref": "#/definitions/Repository", "description": "Optional repository metadata for the MCP server source code. Recommended for transparency and security inspection." @@ -432,7 +426,7 @@ "type": "string", "format": "uri", "description": "JSON Schema URI for this server.json format", - "example": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json" + "example": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json" }, "packages": { "type": "array", @@ -462,11 +456,6 @@ "type": "object", "description": "Publisher-provided metadata for downstream registries", "additionalProperties": true - }, - "io.modelcontextprotocol.registry/official": { - "type": "object", - "description": "Official MCP registry metadata (read-only, added by registry)", - "additionalProperties": true } } } diff --git a/internal/api/handlers/v0/auth/none.go b/internal/api/handlers/v0/auth/none.go index 85f768d..dd09a34 100644 --- a/internal/api/handlers/v0/auth/none.go +++ b/internal/api/handlers/v0/auth/none.go @@ -26,6 +26,8 @@ func NewNoneHandler(cfg *config.Config) *NoneHandler { } // RegisterNoneEndpoint registers the anonymous authentication endpoint +// WARNING: This endpoint is intended for local development and automated tests only. +// It should NOT be enabled in production environments as it bypasses normal authentication. func RegisterNoneEndpoint(api huma.API, cfg *config.Config) { if !cfg.EnableAnonymousAuth { return @@ -33,13 +35,13 @@ func RegisterNoneEndpoint(api huma.API, cfg *config.Config) { handler := NewNoneHandler(cfg) - // Anonymous token endpoint + // Anonymous token endpoint for development/testing only huma.Register(api, huma.Operation{ OperationID: "get-anonymous-token", Method: http.MethodPost, Path: "/v0/auth/none", - Summary: "Get anonymous Registry JWT", - Description: "Get a short-lived Registry JWT token for publishing to io.modelcontextprotocol.anonymous/* namespace", + Summary: "Get anonymous Registry JWT (Development/Testing Only)", + Description: "Get a short-lived Registry JWT token for publishing and editing servers in the io.modelcontextprotocol.anonymous/* namespace. This endpoint is intended for local development and automated testing only.", Tags: []string{"auth"}, }, func(ctx context.Context, _ *struct{}) (*v0.Response[auth.TokenResponse], error) { response, err := handler.GetAnonymousToken(ctx) @@ -61,6 +63,10 @@ func (h *NoneHandler) GetAnonymousToken(ctx context.Context) (*auth.TokenRespons Action: auth.PermissionActionPublish, ResourcePattern: "io.modelcontextprotocol.anonymous/*", }, + { + Action: auth.PermissionActionEdit, + ResourcePattern: "io.modelcontextprotocol.anonymous/*", + }, } // Create JWT claims for anonymous user diff --git a/internal/api/handlers/v0/auth/none_test.go b/internal/api/handlers/v0/auth/none_test.go index 4d1478f..d16f7e8 100644 --- a/internal/api/handlers/v0/auth/none_test.go +++ b/internal/api/handlers/v0/auth/none_test.go @@ -43,8 +43,14 @@ func TestNoneHandler_GetAnonymousToken(t *testing.T) { assert.Equal(t, auth.MethodNone, claims.AuthMethod) assert.Equal(t, "anonymous", claims.AuthMethodSubject) - // Check permissions - require.Len(t, claims.Permissions, 1) + // Check permissions - should have both publish and edit permissions + require.Len(t, claims.Permissions, 2) + + // Check publish permission assert.Equal(t, auth.PermissionActionPublish, claims.Permissions[0].Action) assert.Equal(t, "io.modelcontextprotocol.anonymous/*", claims.Permissions[0].ResourcePattern) + + // Check edit permission + assert.Equal(t, auth.PermissionActionEdit, claims.Permissions[1].Action) + assert.Equal(t, "io.modelcontextprotocol.anonymous/*", claims.Permissions[1].ResourcePattern) } diff --git a/internal/api/handlers/v0/edit.go b/internal/api/handlers/v0/edit.go index 4a3c58c..eb081ec 100644 --- a/internal/api/handlers/v0/edit.go +++ b/internal/api/handlers/v0/edit.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "net/url" "strings" "github.com/danielgtaylor/huma/v2" @@ -18,8 +19,9 @@ import ( // EditServerInput represents the input for editing a server type EditServerInput struct { Authorization string `header:"Authorization" doc:"Registry JWT token with edit permissions" required:"true"` - ServerID string `path:"server_id" doc:"Server ID (UUID)" format:"uuid"` - Version string `query:"version" doc:"Version to edit (e.g., '1.0.0')" required:"true" example:"1.0.0"` + ServerName string `path:"serverName" doc:"URL-encoded server name" example:"com.example%2Fmy-server"` + Version string `path:"version" doc:"URL-encoded version to edit" example:"1.0.0"` + Status string `query:"status" doc:"New status for the server (active, deprecated, deleted)" required:"false" enum:"active,deprecated,deleted"` Body apiv0.ServerJSON `body:""` } @@ -31,14 +33,14 @@ func RegisterEditEndpoints(api huma.API, registry service.RegistryService, cfg * huma.Register(api, huma.Operation{ OperationID: "edit-server", Method: http.MethodPut, - Path: "/v0/servers/{server_id}", + Path: "/v0/servers/{serverName}/versions/{version}", Summary: "Edit MCP server", - Description: "Update an existing MCP server (admin only). Requires version query parameter to specify which version to edit.", + Description: "Update a specific version of an existing MCP server (admin only).", Tags: []string{"admin"}, Security: []map[string][]string{ {"bearer": {}}, }, - }, func(ctx context.Context, input *EditServerInput) (*Response[apiv0.ServerJSON], error) { + }, func(ctx context.Context, input *EditServerInput) (*Response[apiv0.ServerResponse], error) { // Extract bearer token const bearerPrefix = "Bearer " authHeader := input.Authorization @@ -53,9 +55,20 @@ func RegisterEditEndpoints(api huma.API, registry service.RegistryService, cfg * return nil, huma.Error401Unauthorized("Invalid or expired Registry JWT token", err) } - // Get current server to check permissions against existing name - currentServer, err := registry.GetByServerIDAndVersion(input.ServerID, input.Version) + // URL-decode the server name + serverName, err := url.PathUnescape(input.ServerName) + if err != nil { + return nil, huma.Error400BadRequest("Invalid server name encoding", err) + } + // URL-decode the version + version, err := url.PathUnescape(input.Version) + if err != nil { + return nil, huma.Error400BadRequest("Invalid version encoding", err) + } + + // Get current server to check permissions against existing name + currentServer, err := registry.GetServerByNameAndVersion(ctx, serverName, version) if err != nil { if errors.Is(err, database.ErrNotFound) { return nil, huma.Error404NotFound("Server not found") @@ -64,27 +77,42 @@ func RegisterEditEndpoints(api huma.API, registry service.RegistryService, cfg * } // Verify edit permissions for this server using the existing server name - if !jwtManager.HasPermission(currentServer.Name, auth.PermissionActionEdit, claims.Permissions) { + if !jwtManager.HasPermission(currentServer.Server.Name, auth.PermissionActionEdit, claims.Permissions) { return nil, huma.Error403Forbidden("You do not have edit permissions for this server") } // Prevent renaming servers - if currentServer.Name != input.Body.Name { + if currentServer.Server.Name != input.Body.Name { return nil, huma.Error400BadRequest("Cannot rename server") } - // Prevent undeleting servers - once deleted, they stay deleted - if currentServer.Status == model.StatusDeleted && input.Body.Status != model.StatusDeleted { - return nil, huma.Error400BadRequest("Cannot change status of deleted server. Deleted servers cannot be undeleted.") + // Validate that the version in the body matches the URL parameter + if input.Body.Version != version { + return nil, huma.Error400BadRequest("Version in request body must match URL path parameter") } - // Edit the server using the version_id from the current server - versionID := currentServer.GetVersionID() - if versionID == "" { - return nil, huma.Error500InternalServerError("Server version ID not found in metadata", nil) + // Handle status changes with proper permission validation + if input.Status != "" { + newStatus := model.Status(input.Status) + + // Prevent undeleting servers - once deleted, they stay deleted + if currentServer.Meta.Official != nil && + currentServer.Meta.Official.Status == model.StatusDeleted && + newStatus != model.StatusDeleted { + return nil, huma.Error400BadRequest("Cannot change status of deleted server. Deleted servers cannot be undeleted.") + } + + // For now, only allow status changes for admins + // Future: Implement logic to allow server authors to change active <-> deprecated + // but only admins can set to deleted } - updatedServer, err := registry.EditServer(versionID, input.Body) + // Update the server using the service + var statusPtr *string + if input.Status != "" { + statusPtr = &input.Status + } + updatedServer, err := registry.UpdateServer(ctx, serverName, version, &input.Body, statusPtr) if err != nil { if errors.Is(err, database.ErrNotFound) { return nil, huma.Error404NotFound("Server not found") @@ -92,7 +120,7 @@ func RegisterEditEndpoints(api huma.API, registry service.RegistryService, cfg * return nil, huma.Error400BadRequest("Failed to edit server", err) } - return &Response[apiv0.ServerJSON]{ + return &Response[apiv0.ServerResponse]{ Body: *updatedServer, }, nil }) diff --git a/internal/api/handlers/v0/edit_test.go b/internal/api/handlers/v0/edit_test.go index 77fcb3b..5b7b173 100644 --- a/internal/api/handlers/v0/edit_test.go +++ b/internal/api/handlers/v0/edit_test.go @@ -2,14 +2,20 @@ package v0_test import ( "bytes" + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/hex" "encoding/json" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humago" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" "github.com/modelcontextprotocol/registry/internal/auth" @@ -21,313 +27,616 @@ import ( ) func TestEditServerEndpoint(t *testing.T) { - // Create registry service and insert a common test server - registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) - - // Publish a test server that will be used across test cases - testServer := apiv0.ServerJSON{ - Name: "io.github.domdomegg/test-server", - Description: "Original test server", - Status: model.StatusActive, - Repository: model.Repository{ - URL: "https://github.com/domdomegg/test-server", - Source: "github", - ID: "domdomegg/test-server", - }, - Version: "1.0.0", + // Create test config + testSeed := make([]byte, ed25519.SeedSize) + _, err := rand.Read(testSeed) + require.NoError(t, err) + cfg := &config.Config{ + JWTPrivateKey: hex.EncodeToString(testSeed), + EnableRegistryValidation: false, } - published, err := registryService.Publish(testServer) - assert.NoError(t, err) - assert.NotNil(t, published) - assert.NotNil(t, published.Meta) - assert.NotNil(t, published.Meta.Official) - testServerID := published.Meta.Official.ServerID + // Create registry service and test data + registryService := service.NewRegistryService(database.NewTestDB(t), cfg) - // Publish a second server for permission testing - otherServer := apiv0.ServerJSON{ - Name: "io.github.other/test-server", - Description: "Other test server", - Status: model.StatusActive, - Repository: model.Repository{ - URL: "https://github.com/other/test-server", - Source: "github", - ID: "other/test-server", + // Create test servers for different scenarios + testServers := map[string]*apiv0.ServerJSON{ + "editable": { + Name: "io.github.testuser/editable-server", + Description: "Server that can be edited", + Version: "1.0.0", + Repository: model.Repository{ + URL: "https://github.com/testuser/editable-server", + Source: "github", + ID: "testuser/editable-server", + }, }, - Version: "1.0.0", - } - otherPublished, err := registryService.Publish(otherServer) - assert.NoError(t, err) - assert.NotNil(t, otherPublished) - assert.NotNil(t, otherPublished.Meta) - assert.NotNil(t, otherPublished.Meta.Official) - - otherServerID := otherPublished.Meta.Official.ServerID - - // Publish a deleted server for undelete testing - deletedServer := apiv0.ServerJSON{ - Name: "io.github.domdomegg/deleted-server", - Description: "Deleted test server", - Status: model.StatusDeleted, - Repository: model.Repository{ - URL: "https://github.com/domdomegg/deleted-server", - Source: "github", - ID: "domdomegg/deleted-server", + "other": { + Name: "io.github.otheruser/other-server", + Description: "Server owned by another user", + Version: "1.0.0", + Repository: model.Repository{ + URL: "https://github.com/otheruser/other-server", + Source: "github", + ID: "otheruser/other-server", + }, }, - Version: "1.0.0", } - deletedPublished, err := registryService.Publish(deletedServer) - assert.NoError(t, err) - assert.NotNil(t, deletedPublished) - assert.NotNil(t, deletedPublished.Meta) - assert.NotNil(t, deletedPublished.Meta.Official) - deletedServerID := deletedPublished.Meta.Official.ServerID + // Create the test servers + for _, server := range testServers { + _, err := registryService.CreateServer(context.Background(), server) + require.NoError(t, err) + } + + // Create a deleted server for undelete testing + deletedServer := &apiv0.ServerJSON{ + Name: "io.github.testuser/deleted-server", + Description: "Server that was deleted", + Version: "1.0.0", + Repository: model.Repository{ + URL: "https://github.com/testuser/deleted-server", + Source: "github", + ID: "testuser/deleted-server", + }, + } + _, err = registryService.CreateServer(context.Background(), deletedServer) + require.NoError(t, err) + + // Set the server to deleted status + _, err = registryService.UpdateServer(context.Background(), deletedServer.Name, deletedServer.Version, deletedServer, stringPtr(string(model.StatusDeleted))) + require.NoError(t, err) + + // Create a server with build metadata for URL encoding test + buildMetadataServer := &apiv0.ServerJSON{ + Name: "io.github.testuser/build-metadata-server", + Description: "Server with build metadata version", + Version: "1.0.0+20130313144700", + Repository: model.Repository{ + URL: "https://github.com/testuser/build-metadata-server", + Source: "github", + ID: "testuser/build-metadata-server", + }, + } + _, err = registryService.CreateServer(context.Background(), buildMetadataServer) + require.NoError(t, err) testCases := []struct { name string - authHeader string - requestBody interface{} - serverID string + serverName string version string + authClaims *auth.JWTClaims + authHeader string + requestBody apiv0.ServerJSON + statusParam string expectedStatus int expectedError string + checkResult func(*testing.T, *apiv0.ServerResponse) }{ { - name: "successful edit with valid token and permissions", - authHeader: func() string { - cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"} - token, _ := generateTestJWTToken(cfg, auth.JWTClaims{ - AuthMethod: auth.MethodGitHubAT, - AuthMethodSubject: "domdomegg", - Permissions: []auth.Permission{ - {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.domdomegg/*"}, - }, - }) - return "Bearer " + token - }(), + name: "successful edit with valid permissions", + serverName: "io.github.testuser/editable-server", + version: "1.0.0", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.testuser/*"}, + }, + }, requestBody: apiv0.ServerJSON{ - Name: "io.github.domdomegg/test-server", - Description: "Updated test server", - Status: model.StatusDeprecated, + Name: "io.github.testuser/editable-server", + Description: "Updated server description", + Version: "1.0.0", Repository: model.Repository{ - URL: "https://github.com/domdomegg/test-server", + URL: "https://github.com/testuser/editable-server", Source: "github", - ID: "domdomegg/test-server", + ID: "testuser/editable-server", }, - Version: "1.0.0", }, - serverID: testServerID, - version: "1.0.0", expectedStatus: http.StatusOK, + checkResult: func(t *testing.T, resp *apiv0.ServerResponse) { + t.Helper() + assert.Equal(t, "Updated server description", resp.Server.Description) + assert.Equal(t, "io.github.testuser/editable-server", resp.Server.Name) + assert.Equal(t, "1.0.0", resp.Server.Version) + assert.NotNil(t, resp.Meta.Official) + }, + }, + { + name: "successful edit with status change", + serverName: "io.github.testuser/editable-server", + version: "1.0.0", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.testuser/*"}, + }, + }, + requestBody: apiv0.ServerJSON{ + Name: "io.github.testuser/editable-server", + Description: "Server with status change", + Version: "1.0.0", + }, + statusParam: "deprecated", + expectedStatus: http.StatusOK, + checkResult: func(t *testing.T, resp *apiv0.ServerResponse) { + t.Helper() + assert.Equal(t, "Server with status change", resp.Server.Description) + assert.Equal(t, model.StatusDeprecated, resp.Meta.Official.Status) + }, }, { name: "missing authorization header", - authHeader: "", - requestBody: apiv0.ServerJSON{}, - serverID: testServerID, + serverName: "io.github.testuser/editable-server", version: "1.0.0", - expectedStatus: 422, + authHeader: "", // No auth header + requestBody: apiv0.ServerJSON{}, + expectedStatus: http.StatusUnprocessableEntity, expectedError: "required header parameter is missing", }, { name: "invalid authorization header format", + serverName: "io.github.testuser/editable-server", + version: "1.0.0", authHeader: "InvalidFormat token123", requestBody: apiv0.ServerJSON{ - Name: "io.github.domdomegg/test-server", + Name: "io.github.testuser/editable-server", Description: "Test server", Version: "1.0.0", }, - serverID: testServerID, - version: "1.0.0", expectedStatus: http.StatusUnauthorized, - expectedError: "Unauthorized", + expectedError: "Invalid Authorization header format", }, { name: "invalid token", + serverName: "io.github.testuser/editable-server", + version: "1.0.0", authHeader: "Bearer invalid-token", requestBody: apiv0.ServerJSON{ - Name: "io.github.domdomegg/test-server", + Name: "io.github.testuser/editable-server", Description: "Test server", Version: "1.0.0", }, - serverID: testServerID, - version: "1.0.0", expectedStatus: http.StatusUnauthorized, - expectedError: "Unauthorized", + expectedError: "Invalid or expired Registry JWT token", }, { - name: "permission denied - no edit permissions", - authHeader: func() string { - cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"} - token, _ := generateTestJWTToken(cfg, auth.JWTClaims{ - AuthMethod: auth.MethodGitHubAT, - AuthMethodSubject: "domdomegg", - Permissions: []auth.Permission{ - {Action: auth.PermissionActionPublish, ResourcePattern: "io.github.domdomegg/*"}, - }, - }) - return "Bearer " + token - }(), - requestBody: apiv0.ServerJSON{ - Name: "io.github.domdomegg/test-server", - Description: "Updated test server", - Version: "1.0.0", - }, - serverID: testServerID, - version: "1.0.0", - expectedStatus: http.StatusForbidden, - expectedError: "Forbidden", - }, - { - name: "permission denied - wrong resource", - authHeader: func() string { - cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"} - token, _ := generateTestJWTToken(cfg, auth.JWTClaims{ - AuthMethod: auth.MethodGitHubAT, - AuthMethodSubject: "domdomegg", - Permissions: []auth.Permission{ - {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.domdomegg/*"}, - }, - }) - return "Bearer " + token - }(), - requestBody: apiv0.ServerJSON{ - Name: "io.github.other/test-server", - Description: "Updated test server", - Version: "1.0.0", - }, - serverID: otherServerID, - version: "1.0.0", - expectedStatus: http.StatusForbidden, - expectedError: "Forbidden", - }, - { - name: "server not found", - authHeader: func() string { - cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"} - token, _ := generateTestJWTToken(cfg, auth.JWTClaims{ - AuthMethod: auth.MethodGitHubAT, - AuthMethodSubject: "domdomegg", - Permissions: []auth.Permission{ - {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.domdomegg/*"}, - }, - }) - return "Bearer " + token - }(), - requestBody: apiv0.ServerJSON{ - Name: "io.github.domdomegg/nonexistent-server", - Description: "Updated test server", - Version: "1.0.0", - }, - serverID: "550e8400-e29b-41d4-a716-446655440999", // Non-existent ID - version: "1.0.0", - expectedStatus: http.StatusNotFound, - expectedError: "Not Found", - }, - { - name: "validation error - invalid server name", - authHeader: func() string { - cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"} - token, _ := generateTestJWTToken(cfg, auth.JWTClaims{ - AuthMethod: auth.MethodGitHubAT, - AuthMethodSubject: "domdomegg", - Permissions: []auth.Permission{ - {Action: auth.PermissionActionEdit, ResourcePattern: "*"}, - }, - }) - return "Bearer " + token - }(), - requestBody: apiv0.ServerJSON{ - Name: "invalid-name-format", // Missing namespace/name format - Description: "Test server", - Version: "1.0.0", - }, - serverID: testServerID, - version: "1.0.0", - expectedStatus: http.StatusBadRequest, - expectedError: "Bad Request", - }, - { - name: "cannot undelete server", - authHeader: func() string { - cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"} - token, _ := generateTestJWTToken(cfg, auth.JWTClaims{ - AuthMethod: auth.MethodGitHubAT, - AuthMethodSubject: "domdomegg", - Permissions: []auth.Permission{ - {Action: auth.PermissionActionEdit, ResourcePattern: "*"}, - }, - }) - return "Bearer " + token - }(), - requestBody: apiv0.ServerJSON{ - Name: "io.github.domdomegg/deleted-server", - Description: "Trying to undelete server", - Status: model.StatusActive, - Repository: model.Repository{ - URL: "https://github.com/domdomegg/deleted-server", - Source: "github", - ID: "domdomegg/deleted-server", + name: "permission denied - no edit permissions", + serverName: "io.github.testuser/editable-server", + version: "1.0.0", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionPublish, ResourcePattern: "io.github.testuser/*"}, // Only publish, not edit }, - Version: "1.0.1", }, - serverID: deletedServerID, - version: "1.0.0", + requestBody: apiv0.ServerJSON{ + Name: "io.github.testuser/editable-server", + Description: "Updated test server", + Version: "1.0.0", + }, + expectedStatus: http.StatusForbidden, + expectedError: "You do not have edit permissions", + }, + { + name: "permission denied - wrong namespace", + serverName: "io.github.otheruser/other-server", + version: "1.0.0", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.testuser/*"}, // Wrong namespace + }, + }, + requestBody: apiv0.ServerJSON{ + Name: "io.github.otheruser/other-server", + Description: "Updated test server", + Version: "1.0.0", + }, + expectedStatus: http.StatusForbidden, + expectedError: "You do not have edit permissions", + }, + { + name: "server not found", + serverName: "io.github.testuser/non-existent", + version: "1.0.0", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.testuser/*"}, + }, + }, + requestBody: apiv0.ServerJSON{ + Name: "io.github.testuser/non-existent", + Description: "Non-existent server", + Version: "1.0.0", + }, + expectedStatus: http.StatusNotFound, + expectedError: "Server not found", + }, + { + name: "attempt to rename server should fail", + serverName: "io.github.testuser/editable-server", + version: "1.0.0", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.testuser/*"}, + }, + }, + requestBody: apiv0.ServerJSON{ + Name: "io.github.testuser/renamed-server", // Different name + Description: "Trying to rename server", + Version: "1.0.0", + }, + expectedStatus: http.StatusBadRequest, + expectedError: "Cannot rename server", + }, + { + name: "version in body must match URL parameter", + serverName: "io.github.testuser/editable-server", + version: "1.0.0", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.testuser/*"}, + }, + }, + requestBody: apiv0.ServerJSON{ + Name: "io.github.testuser/editable-server", + Description: "Version mismatch test", + Version: "2.0.0", // Different version from URL + }, + expectedStatus: http.StatusBadRequest, + expectedError: "Version in request body must match URL path parameter", + }, + { + name: "attempt to undelete server should fail", + serverName: "io.github.testuser/deleted-server", + version: "1.0.0", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.testuser/*"}, + }, + }, + requestBody: apiv0.ServerJSON{ + Name: "io.github.testuser/deleted-server", + Description: "Trying to undelete server", + Version: "1.0.0", + }, + statusParam: "active", // Trying to change from deleted to active expectedStatus: http.StatusBadRequest, expectedError: "Cannot change status of deleted server", }, + { + name: "successful edit of version with build metadata (URL encoded)", + serverName: "io.github.testuser/build-metadata-server", + version: "1.0.0+20130313144700", + authClaims: &auth.JWTClaims{ + AuthMethod: auth.MethodGitHubAT, + AuthMethodSubject: "testuser", + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "io.github.testuser/*"}, + }, + }, + requestBody: apiv0.ServerJSON{ + Name: "io.github.testuser/build-metadata-server", + Description: "Updated server with build metadata", + Version: "1.0.0+20130313144700", + Repository: model.Repository{ + URL: "https://github.com/testuser/build-metadata-server", + Source: "github", + ID: "testuser/build-metadata-server", + }, + }, + expectedStatus: http.StatusOK, + checkResult: func(t *testing.T, resp *apiv0.ServerResponse) { + t.Helper() + assert.Equal(t, "Updated server with build metadata", resp.Server.Description) + assert.Equal(t, "io.github.testuser/build-metadata-server", resp.Server.Name) + assert.Equal(t, "1.0.0+20130313144700", resp.Server.Version) + assert.NotNil(t, resp.Meta.Official) + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Create Huma API mux := http.NewServeMux() - humaConfig := huma.DefaultConfig("Test API", "1.0.0") - api := humago.New(mux, humaConfig) + api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) // Register edit endpoints - cfg := &config.Config{ - JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c", - } v0.RegisterEditEndpoints(api, registryService, cfg) // Create request body - var requestBody []byte - var err error - if str, ok := tc.requestBody.(string); ok { - requestBody = []byte(str) - } else { - requestBody, err = json.Marshal(tc.requestBody) - assert.NoError(t, err) + requestBody, err := json.Marshal(tc.requestBody) + require.NoError(t, err) + + // Create request URL with proper encoding + encodedServerName := url.PathEscape(tc.serverName) + encodedVersion := url.PathEscape(tc.version) + requestURL := "/v0/servers/" + encodedServerName + "/versions/" + encodedVersion + if tc.statusParam != "" { + requestURL += "?status=" + tc.statusParam } - // Create request - url := "/v0/servers/" + tc.serverID - if tc.version != "" { - url += "?version=" + tc.version - } - req := httptest.NewRequest(http.MethodPut, url, bytes.NewReader(requestBody)) + req := httptest.NewRequest(http.MethodPut, requestURL, bytes.NewReader(requestBody)) req.Header.Set("Content-Type", "application/json") + + // Set authorization header if tc.authHeader != "" { req.Header.Set("Authorization", tc.authHeader) + } else if tc.authClaims != nil { + // Generate valid JWT token + jwtManager := auth.NewJWTManager(cfg) + tokenResponse, err := jwtManager.GenerateTokenResponse(context.Background(), *tc.authClaims) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenResponse.RegistryToken) } - // Create response recorder + // Create response recorder and execute request w := httptest.NewRecorder() - - // Call the endpoint mux.ServeHTTP(w, req) - // Check status code + // Check response assert.Equal(t, tc.expectedStatus, w.Code) - // Check error message if expected if tc.expectedError != "" { assert.Contains(t, w.Body.String(), tc.expectedError) } - // No mock assertions needed with real service + if tc.expectedStatus == http.StatusOK && tc.checkResult != nil { + var response apiv0.ServerResponse + err := json.NewDecoder(w.Body).Decode(&response) + require.NoError(t, err) + tc.checkResult(t, &response) + } }) } } + +func TestEditServerEndpointEdgeCases(t *testing.T) { + // Create test config + testSeed := make([]byte, ed25519.SeedSize) + _, err := rand.Read(testSeed) + require.NoError(t, err) + cfg := &config.Config{ + JWTPrivateKey: hex.EncodeToString(testSeed), + EnableRegistryValidation: false, + } + + // Create registry service + registryService := service.NewRegistryService(database.NewTestDB(t), cfg) + + // Setup test servers with different characteristics + testServers := []struct { + name string + version string + status model.Status + }{ + {"com.example/active-server", "1.0.0", model.StatusActive}, + {"com.example/deprecated-server", "1.0.0", model.StatusDeprecated}, + {"com.example/multi-version-server", "1.0.0", model.StatusActive}, + {"com.example/multi-version-server", "2.0.0", model.StatusActive}, + } + + for _, server := range testServers { + _, err := registryService.CreateServer(context.Background(), &apiv0.ServerJSON{ + Name: server.name, + Description: "Test server for editing", + Version: server.version, + }) + require.NoError(t, err) + + // Set specific status if not active + if server.status != model.StatusActive { + _, err = registryService.UpdateServer(context.Background(), server.name, server.version, &apiv0.ServerJSON{ + Name: server.name, + Description: "Test server for editing", + Version: server.version, + }, stringPtr(string(server.status))) + require.NoError(t, err) + } + } + + // Create API + mux := http.NewServeMux() + api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) + v0.RegisterEditEndpoints(api, registryService, cfg) + + t.Run("status transitions", func(t *testing.T) { + tests := []struct { + name string + serverName string + version string + fromStatus string + toStatus string + expectedStatus int + expectedError string + }{ + { + name: "active to deprecated", + serverName: "com.example/active-server", + version: "1.0.0", + toStatus: "deprecated", + expectedStatus: http.StatusOK, + }, + { + name: "deprecated to active", + serverName: "com.example/deprecated-server", + version: "1.0.0", + toStatus: "active", + expectedStatus: http.StatusOK, + }, + { + name: "active to deleted", + serverName: "com.example/active-server", + version: "1.0.0", + toStatus: "deleted", + expectedStatus: http.StatusOK, + }, + { + name: "invalid status", + serverName: "com.example/active-server", + version: "1.0.0", + toStatus: "invalid_status", + expectedStatus: http.StatusUnprocessableEntity, + expectedError: "validation failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + requestBody := apiv0.ServerJSON{ + Name: tt.serverName, + Description: "Status transition test", + Version: tt.version, + } + + bodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + encodedName := url.PathEscape(tt.serverName) + requestURL := "/v0/servers/" + encodedName + "/versions/" + tt.version + "?status=" + tt.toStatus + + req := httptest.NewRequest(http.MethodPut, requestURL, bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Generate admin token + jwtManager := auth.NewJWTManager(cfg) + tokenResponse, err := jwtManager.GenerateTokenResponse(context.Background(), auth.JWTClaims{ + AuthMethod: auth.MethodNone, + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "*"}, + }, + }) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenResponse.RegistryToken) + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + + if tt.expectedError != "" { + assert.Contains(t, w.Body.String(), tt.expectedError) + } + + if tt.expectedStatus == http.StatusOK { + var response apiv0.ServerResponse + err := json.NewDecoder(w.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, model.Status(tt.toStatus), response.Meta.Official.Status) + } + }) + } + }) + + t.Run("URL encoding edge cases", func(t *testing.T) { + // Create server with special characters + specialServerName := "io.dots.and-dashes/server_with_underscores" + _, err := registryService.CreateServer(context.Background(), &apiv0.ServerJSON{ + Name: specialServerName, + Description: "Server with special characters", + Version: "1.0.0", + }) + require.NoError(t, err) + + requestBody := apiv0.ServerJSON{ + Name: specialServerName, + Description: "Updated server with special chars", + Version: "1.0.0", + } + + bodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + encodedName := url.PathEscape(specialServerName) + requestURL := "/v0/servers/" + encodedName + "/versions/1.0.0" + + req := httptest.NewRequest(http.MethodPut, requestURL, bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Generate admin token + jwtManager := auth.NewJWTManager(cfg) + tokenResponse, err := jwtManager.GenerateTokenResponse(context.Background(), auth.JWTClaims{ + AuthMethod: auth.MethodNone, + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "*"}, + }, + }) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenResponse.RegistryToken) + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response apiv0.ServerResponse + err = json.NewDecoder(w.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, specialServerName, response.Server.Name) + assert.Equal(t, "Updated server with special chars", response.Server.Description) + }) + + t.Run("version-specific editing", func(t *testing.T) { + // Test editing a specific version of a multi-version server + requestBody := apiv0.ServerJSON{ + Name: "com.example/multi-version-server", + Description: "Updated v1.0.0 specifically", + Version: "1.0.0", + } + + bodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + encodedName := url.PathEscape("com.example/multi-version-server") + requestURL := "/v0/servers/" + encodedName + "/versions/1.0.0" + + req := httptest.NewRequest(http.MethodPut, requestURL, bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Generate admin token + jwtManager := auth.NewJWTManager(cfg) + tokenResponse, err := jwtManager.GenerateTokenResponse(context.Background(), auth.JWTClaims{ + AuthMethod: auth.MethodNone, + Permissions: []auth.Permission{ + {Action: auth.PermissionActionEdit, ResourcePattern: "*"}, + }, + }) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenResponse.RegistryToken) + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response apiv0.ServerResponse + err = json.NewDecoder(w.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "Updated v1.0.0 specifically", response.Server.Description) + assert.Equal(t, "1.0.0", response.Server.Version) + + // Verify the other version wasn't affected + otherVersion, err := registryService.GetServerByNameAndVersion(context.Background(), "com.example/multi-version-server", "2.0.0") + require.NoError(t, err) + assert.NotEqual(t, "Updated v1.0.0 specifically", otherVersion.Server.Description) + }) +} + +// Helper function +func stringPtr(s string) *string { + return &s +} \ No newline at end of file diff --git a/internal/api/handlers/v0/publish.go b/internal/api/handlers/v0/publish.go index 864f308..2a0e7a9 100644 --- a/internal/api/handlers/v0/publish.go +++ b/internal/api/handlers/v0/publish.go @@ -33,7 +33,7 @@ func RegisterPublishEndpoint(api huma.API, registry service.RegistryService, cfg Security: []map[string][]string{ {"bearer": {}}, }, - }, func(ctx context.Context, input *PublishServerInput) (*Response[apiv0.ServerJSON], error) { + }, func(ctx context.Context, input *PublishServerInput) (*Response[apiv0.ServerResponse], error) { // Extract bearer token const bearerPrefix = "Bearer " authHeader := input.Authorization @@ -54,13 +54,13 @@ func RegisterPublishEndpoint(api huma.API, registry service.RegistryService, cfg } // Publish the server with extensions - publishedServer, err := registry.Publish(input.Body) + publishedServer, err := registry.CreateServer(ctx, &input.Body) if err != nil { return nil, huma.Error400BadRequest("Failed to publish server", err) } - // Return the published server in flattened format - return &Response[apiv0.ServerJSON]{ + // Return the published server response with metadata + return &Response[apiv0.ServerResponse]{ Body: *publishedServer, }, nil }) diff --git a/internal/api/handlers/v0/publish_integration_test.go b/internal/api/handlers/v0/publish_integration_test.go index 44d628f..083258f 100644 --- a/internal/api/handlers/v0/publish_integration_test.go +++ b/internal/api/handlers/v0/publish_integration_test.go @@ -90,12 +90,13 @@ func TestPublishIntegration(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) - var response apiv0.ServerJSON + var response apiv0.ServerResponse err = json.Unmarshal(rr.Body.Bytes(), &response) require.NoError(t, err) - assert.Equal(t, publishReq.Name, response.Name) - assert.Equal(t, publishReq.Version, response.Version) + assert.Equal(t, publishReq.Name, response.Server.Name) + assert.Equal(t, publishReq.Version, response.Server.Version) + assert.NotNil(t, response.Meta.Official) }) t.Run("successful publish with none auth (no prefix)", func(t *testing.T) { @@ -132,11 +133,12 @@ func TestPublishIntegration(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) - var response apiv0.ServerJSON + var response apiv0.ServerResponse err = json.Unmarshal(rr.Body.Bytes(), &response) require.NoError(t, err) - assert.Equal(t, publishReq.Name, response.Name) + assert.Equal(t, publishReq.Name, response.Server.Name) + assert.NotNil(t, response.Meta.Official) }) t.Run("publish fails with missing authorization header", func(t *testing.T) { @@ -220,7 +222,6 @@ func TestPublishIntegration(t *testing.T) { Name: "io.github.domdomegg/airtable-mcp-server", Description: "A test server with MCPB package", Version: "1.7.2", - Status: model.StatusActive, Packages: []model.Package{ { RegistryType: model.RegistryTypeMCPB, @@ -256,13 +257,14 @@ func TestPublishIntegration(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) - var response apiv0.ServerJSON + var response apiv0.ServerResponse err = json.Unmarshal(rr.Body.Bytes(), &response) require.NoError(t, err) - assert.Equal(t, publishReq.Name, response.Name) - assert.Equal(t, publishReq.Version, response.Version) - assert.Len(t, response.Packages, 1) - assert.Equal(t, model.RegistryTypeMCPB, response.Packages[0].RegistryType) + assert.Equal(t, publishReq.Name, response.Server.Name) + assert.Equal(t, publishReq.Version, response.Server.Version) + assert.NotNil(t, response.Meta.Official) + assert.Len(t, response.Server.Packages, 1) + assert.Equal(t, model.RegistryTypeMCPB, response.Server.Packages[0].RegistryType) }) } diff --git a/internal/api/handlers/v0/publish_registry_validation_test.go b/internal/api/handlers/v0/publish_registry_validation_test.go index 93495cb..91530db 100644 --- a/internal/api/handlers/v0/publish_registry_validation_test.go +++ b/internal/api/handlers/v0/publish_registry_validation_test.go @@ -124,14 +124,14 @@ func TestPublishRegistryValidation(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) - var response apiv0.ServerJSON + var response apiv0.ServerResponse err = json.Unmarshal(rr.Body.Bytes(), &response) require.NoError(t, err) - assert.Equal(t, publishReq.Name, response.Name) - assert.Equal(t, publishReq.Version, response.Version) - assert.Len(t, response.Packages, 1) - assert.Equal(t, model.RegistryTypeMCPB, response.Packages[0].RegistryType) + assert.Equal(t, publishReq.Name, response.Server.Name) + assert.Equal(t, publishReq.Version, response.Server.Version) + assert.Len(t, response.Server.Packages, 1) + assert.Equal(t, model.RegistryTypeMCPB, response.Server.Packages[0].RegistryType) }) t.Run("publish fails when second package fails npm validation", func(t *testing.T) { diff --git a/internal/api/handlers/v0/publish_test.go b/internal/api/handlers/v0/publish_test.go index 854f3cd..d47e999 100644 --- a/internal/api/handlers/v0/publish_test.go +++ b/internal/api/handlers/v0/publish_test.go @@ -192,7 +192,7 @@ func TestPublishEndpoint(t *testing.T) { ID: "example/test-server-existing", }, } - _, _ = registry.Publish(existingServer) + _, _ = registry.CreateServer(context.Background(), &existingServer) }, expectedStatus: http.StatusBadRequest, expectedError: "invalid version: cannot publish duplicate version", diff --git a/internal/api/handlers/v0/servers.go b/internal/api/handlers/v0/servers.go index ed4bc62..3734f5f 100644 --- a/internal/api/handlers/v0/servers.go +++ b/internal/api/handlers/v0/servers.go @@ -4,18 +4,20 @@ import ( "context" "errors" "net/http" + "net/url" "time" "github.com/danielgtaylor/huma/v2" - "github.com/google/uuid" "github.com/modelcontextprotocol/registry/internal/database" "github.com/modelcontextprotocol/registry/internal/service" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" ) +const errRecordNotFound = "record not found" + // ListServersInput represents the input for listing servers type ListServersInput struct { - Cursor string `query:"cursor" doc:"Pagination cursor (UUID)" format:"uuid" required:"false" example:"550e8400-e29b-41d4-a716-446655440000"` + Cursor string `query:"cursor" doc:"Pagination cursor" required:"false" example:"server-cursor-123"` Limit int `query:"limit" doc:"Number of items per page" default:"30" minimum:"1" maximum:"100" example:"50"` UpdatedSince string `query:"updated_since" doc:"Filter servers updated since timestamp (RFC3339 datetime)" required:"false" example:"2025-08-07T13:15:04.280Z"` Search string `query:"search" doc:"Search servers by name (substring match)" required:"false" example:"filesystem"` @@ -24,16 +26,22 @@ type ListServersInput struct { // ServerDetailInput represents the input for getting server details type ServerDetailInput struct { - ServerID string `path:"server_id" doc:"Server ID (UUID)" format:"uuid"` - Version string `query:"version" doc:"Specific version to retrieve (e.g., '1.0.0'). If not specified, returns latest version." required:"false" example:"1.0.0"` + ServerName string `path:"serverName" doc:"URL-encoded server name" example:"com.example%2Fmy-server"` +} + +// ServerVersionDetailInput represents the input for getting a specific version +type ServerVersionDetailInput struct { + ServerName string `path:"serverName" doc:"URL-encoded server name" example:"com.example%2Fmy-server"` + Version string `path:"version" doc:"URL-encoded server version" example:"1.0.0"` } // ServerVersionsInput represents the input for listing all versions of a server type ServerVersionsInput struct { - ServerID string `path:"server_id" doc:"Server ID (UUID)" format:"uuid"` + ServerName string `path:"serverName" doc:"URL-encoded server name" example:"com.example%2Fmy-server"` } // RegisterServersEndpoints registers all server-related endpoints +//nolint:cyclop // Multiple endpoint registrations are inherently complex func RegisterServersEndpoints(api huma.API, registry service.RegistryService) { // List servers endpoint huma.Register(api, huma.Operation{ @@ -43,15 +51,7 @@ func RegisterServersEndpoints(api huma.API, registry service.RegistryService) { Summary: "List MCP servers", Description: "Get a paginated list of MCP servers from the registry", Tags: []string{"servers"}, - }, func(_ context.Context, input *ListServersInput) (*Response[apiv0.ServerListResponse], error) { - // Validate cursor if provided - if input.Cursor != "" { - _, err := uuid.Parse(input.Cursor) - if err != nil { - return nil, huma.Error400BadRequest("Invalid cursor parameter") - } - } - + }, func(ctx context.Context, input *ListServersInput) (*Response[apiv0.ServerListResponse], error) { // Build filter from input parameters filter := &database.ServerFilter{} @@ -83,14 +83,20 @@ func RegisterServersEndpoints(api huma.API, registry service.RegistryService) { } // Get paginated results with filtering - servers, nextCursor, err := registry.List(filter, input.Cursor, input.Limit) + servers, nextCursor, err := registry.ListServers(ctx, filter, input.Cursor, input.Limit) if err != nil { return nil, huma.Error500InternalServerError("Failed to get registry list", err) } + // Convert []*ServerResponse to []ServerResponse + serverValues := make([]apiv0.ServerResponse, len(servers)) + for i, server := range servers { + serverValues[i] = *server + } + return &Response[apiv0.ServerListResponse]{ Body: apiv0.ServerListResponse{ - Servers: servers, + Servers: serverValues, Metadata: apiv0.Metadata{ NextCursor: nextCursor, Count: len(servers), @@ -99,36 +105,67 @@ func RegisterServersEndpoints(api huma.API, registry service.RegistryService) { }, nil }) - // Get server details endpoint + // Get server details endpoint (latest version) huma.Register(api, huma.Operation{ OperationID: "get-server", Method: http.MethodGet, - Path: "/v0/servers/{server_id}", + Path: "/v0/servers/{serverName}", Summary: "Get MCP server details", - Description: "Get detailed information about a specific MCP server. Returns the latest version by default, or a specific version if the 'version' query parameter is provided.", + Description: "Get detailed information about the latest version of a specific MCP server.", Tags: []string{"servers"}, - }, func(_ context.Context, input *ServerDetailInput) (*Response[apiv0.ServerJSON], error) { - // Get the server details from the registry service - var serverDetail *apiv0.ServerJSON - var err error - - if input.Version != "" { - // Get specific version by server_id and version - serverDetail, err = registry.GetByServerIDAndVersion(input.ServerID, input.Version) - } else { - // Get latest version by server_id - serverDetail, err = registry.GetByServerID(input.ServerID) + }, func(ctx context.Context, input *ServerDetailInput) (*Response[apiv0.ServerResponse], error) { + // URL-decode the server name + serverName, err := url.PathUnescape(input.ServerName) + if err != nil { + return nil, huma.Error400BadRequest("Invalid server name encoding", err) } + // Get latest version by server name + serverResponse, err := registry.GetServerByName(ctx, serverName) if err != nil { - if err.Error() == "record not found" || errors.Is(err, database.ErrNotFound) { + if err.Error() == errRecordNotFound || errors.Is(err, database.ErrNotFound) { return nil, huma.Error404NotFound("Server not found") } return nil, huma.Error500InternalServerError("Failed to get server details", err) } - return &Response[apiv0.ServerJSON]{ - Body: *serverDetail, + return &Response[apiv0.ServerResponse]{ + Body: *serverResponse, + }, nil + }) + + // Get specific server version endpoint + huma.Register(api, huma.Operation{ + OperationID: "get-server-version", + Method: http.MethodGet, + Path: "/v0/servers/{serverName}/versions/{version}", + Summary: "Get specific MCP server version", + Description: "Get detailed information about a specific version of an MCP server.", + Tags: []string{"servers"}, + }, func(ctx context.Context, input *ServerVersionDetailInput) (*Response[apiv0.ServerResponse], error) { + // URL-decode the server name + serverName, err := url.PathUnescape(input.ServerName) + if err != nil { + return nil, huma.Error400BadRequest("Invalid server name encoding", err) + } + + // URL-decode the version + version, err := url.PathUnescape(input.Version) + if err != nil { + return nil, huma.Error400BadRequest("Invalid version encoding", err) + } + + // Get specific version by server name and version + serverResponse, err := registry.GetServerByNameAndVersion(ctx, serverName, version) + if err != nil { + if err.Error() == errRecordNotFound || errors.Is(err, database.ErrNotFound) { + return nil, huma.Error404NotFound("Server not found") + } + return nil, huma.Error500InternalServerError("Failed to get server details", err) + } + + return &Response[apiv0.ServerResponse]{ + Body: *serverResponse, }, nil }) @@ -136,23 +173,35 @@ func RegisterServersEndpoints(api huma.API, registry service.RegistryService) { huma.Register(api, huma.Operation{ OperationID: "get-server-versions", Method: http.MethodGet, - Path: "/v0/servers/{server_id}/versions", + Path: "/v0/servers/{serverName}/versions", Summary: "Get all versions of an MCP server", Description: "Get all available versions for a specific MCP server", Tags: []string{"servers"}, - }, func(_ context.Context, input *ServerVersionsInput) (*Response[apiv0.ServerListResponse], error) { - // Get all versions for this server - servers, err := registry.GetAllVersionsByServerID(input.ServerID) + }, func(ctx context.Context, input *ServerVersionsInput) (*Response[apiv0.ServerListResponse], error) { + // URL-decode the server name + serverName, err := url.PathUnescape(input.ServerName) if err != nil { - if err.Error() == "record not found" { + return nil, huma.Error400BadRequest("Invalid server name encoding", err) + } + + // Get all versions for this server + servers, err := registry.GetAllVersionsByServerName(ctx, serverName) + if err != nil { + if err.Error() == errRecordNotFound || errors.Is(err, database.ErrNotFound) { return nil, huma.Error404NotFound("Server not found") } return nil, huma.Error500InternalServerError("Failed to get server versions", err) } + // Convert []*ServerResponse to []ServerResponse + serverValues := make([]apiv0.ServerResponse, len(servers)) + for i, server := range servers { + serverValues[i] = *server + } + return &Response[apiv0.ServerListResponse]{ Body: apiv0.ServerListResponse{ - Servers: servers, + Servers: serverValues, Metadata: apiv0.Metadata{ Count: len(servers), }, diff --git a/internal/api/handlers/v0/servers_test.go b/internal/api/handlers/v0/servers_test.go index 506cb8f..8dd5b3f 100644 --- a/internal/api/handlers/v0/servers_test.go +++ b/internal/api/handlers/v0/servers_test.go @@ -5,11 +5,11 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humago" - "github.com/google/uuid" v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" "github.com/modelcontextprotocol/registry/internal/config" "github.com/modelcontextprotocol/registry/internal/database" @@ -17,689 +17,496 @@ import ( apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestServersListEndpoint(t *testing.T) { - testCases := []struct { - name string - queryParams string - setupRegistryService func(service.RegistryService) - expectedStatus int - expectedMeta *apiv0.Metadata - expectedError string - }{ - { - name: "successful list with default parameters", - setupRegistryService: func(registry service.RegistryService) { - // Publish test servers - server1 := apiv0.ServerJSON{ - Name: "com.example/test-server-1", - Description: "First test server", - Repository: model.Repository{ - URL: "https://github.com/example/test-server-1", - Source: "github", - ID: "example/test-server-1", - }, - Version: "1.0.0", - } - server2 := apiv0.ServerJSON{ - Name: "com.example/test-server-2", - Description: "Second test server", - Repository: model.Repository{ - URL: "https://github.com/example/test-server-2", - Source: "github", - ID: "example/test-server-2", - }, - Version: "2.0.0", - } - _, _ = registry.Publish(server1) - _, _ = registry.Publish(server2) - }, - expectedStatus: http.StatusOK, - }, - { - name: "successful list with cursor and limit", - queryParams: "?limit=10", - setupRegistryService: func(registry service.RegistryService) { - server := apiv0.ServerJSON{ - Name: "com.example/test-server-3", - Description: "Third test server", - Repository: model.Repository{ - URL: "https://github.com/example/test-server-3", - Source: "github", - ID: "example/test-server-3", - }, - Version: "1.5.0", - } - _, _ = registry.Publish(server) - }, - expectedStatus: http.StatusOK, - }, - { - name: "successful list with limit capping at 100", - queryParams: "?limit=150", - setupRegistryService: func(_ service.RegistryService) {}, - expectedStatus: http.StatusUnprocessableEntity, // Huma rejects values > maximum - expectedError: "validation failed", - }, - { - name: "invalid cursor parameter", - queryParams: "?cursor=invalid-uuid", - setupRegistryService: func(_ service.RegistryService) {}, - expectedStatus: http.StatusUnprocessableEntity, // Huma returns 422 for validation errors - expectedError: "validation failed", - }, - { - name: "invalid limit parameter - non-numeric", - queryParams: "?limit=abc", - setupRegistryService: func(_ service.RegistryService) {}, - expectedStatus: http.StatusUnprocessableEntity, // Huma returns 422 for validation errors - expectedError: "validation failed", - }, - { - name: "invalid limit parameter - zero", - queryParams: "?limit=0", - setupRegistryService: func(_ service.RegistryService) {}, - expectedStatus: http.StatusUnprocessableEntity, // Huma returns 422 for validation errors - expectedError: "validation failed", - }, - { - name: "invalid limit parameter - negative", - queryParams: "?limit=-5", - setupRegistryService: func(_ service.RegistryService) {}, - expectedStatus: http.StatusUnprocessableEntity, // Huma returns 422 for validation errors - expectedError: "validation failed", - }, - { - name: "empty registry returns success", - setupRegistryService: func(_ service.RegistryService) { - // Test empty registry - empty setup - }, - expectedStatus: http.StatusOK, - }, - { - name: "successful search by name substring", - queryParams: "?search=test-server", - setupRegistryService: func(registry service.RegistryService) { - server1 := apiv0.ServerJSON{ - Name: "com.example/test-server-matching", - Description: "Matching test server", - Repository: model.Repository{ - URL: "https://github.com/example/test-matching", - Source: "github", - ID: "example/test-matching", - }, - Version: "1.0.0", - } - server2 := apiv0.ServerJSON{ - Name: "com.example/other-server", - Description: "Non-matching server", - Repository: model.Repository{ - URL: "https://github.com/example/other", - Source: "github", - ID: "example/other", - }, - Version: "1.0.0", - } - _, _ = registry.Publish(server1) - _, _ = registry.Publish(server2) - }, - expectedStatus: http.StatusOK, - }, - { - name: "successful updated_since filter with RFC3339", - queryParams: "?updated_since=2020-01-01T00:00:00Z", - setupRegistryService: func(registry service.RegistryService) { - server := apiv0.ServerJSON{ - Name: "com.example/recent-server", - Description: "Recently updated server", - Repository: model.Repository{ - URL: "https://github.com/example/recent", - Source: "github", - ID: "example/recent", - }, - Version: "1.0.0", - } - _, _ = registry.Publish(server) - }, - expectedStatus: http.StatusOK, - }, - { - name: "successful version=latest filter", - queryParams: "?version=latest", - setupRegistryService: func(registry service.RegistryService) { - server1 := apiv0.ServerJSON{ - Name: "com.example/versioned-server", - Description: "First version", - Repository: model.Repository{ - URL: "https://github.com/example/versioned", - Source: "github", - ID: "example/versioned", - }, - Version: "1.0.0", - } - server2 := apiv0.ServerJSON{ - Name: "com.example/versioned-server", - Description: "Second version (latest)", - Repository: model.Repository{ - URL: "https://github.com/example/versioned", - Source: "github", - ID: "example/versioned", - }, - Version: "2.0.0", - } - _, _ = registry.Publish(server1) - _, _ = registry.Publish(server2) // This will be marked as latest - }, - expectedStatus: http.StatusOK, - }, - { - name: "combined search and updated_since filter", - queryParams: "?search=combined&updated_since=2020-01-01T00:00:00Z", - setupRegistryService: func(registry service.RegistryService) { - server1 := apiv0.ServerJSON{ - Name: "com.example/combined-test", - Description: "Server with combined filtering", - Repository: model.Repository{ - URL: "https://github.com/example/combined", - Source: "github", - ID: "example/combined", - }, - Version: "1.0.0", - } - server2 := apiv0.ServerJSON{ - Name: "com.example/other-server", - Description: "Server that doesn't match search", - Repository: model.Repository{ - URL: "https://github.com/example/nomatch", - Source: "github", - ID: "example/nomatch", - }, - Version: "1.0.0", - } - _, _ = registry.Publish(server1) - _, _ = registry.Publish(server2) - }, - expectedStatus: http.StatusOK, - }, - { - name: "invalid updated_since format", - queryParams: "?updated_since=invalid-timestamp", - setupRegistryService: func(_ service.RegistryService) {}, - expectedStatus: http.StatusBadRequest, - expectedError: "Invalid updated_since format: expected RFC3339", - }, - { - name: "comprehensive query with all parameters", - queryParams: "?search=filesystem&updated_since=2020-01-01T00:00:00Z&version=latest&limit=50&cursor=", - setupRegistryService: func(registry service.RegistryService) { - // Create multiple versions of servers with different names - server1v1 := apiv0.ServerJSON{ - Name: "io.example/filesystem-server", - Description: "Filesystem operations server v1", - Repository: model.Repository{ - URL: "https://github.com/example/filesystem", - Source: "github", - ID: "example/filesystem", - }, - Version: "1.0.0", - } - server1v2 := apiv0.ServerJSON{ - Name: "io.example/filesystem-server", - Description: "Filesystem operations server v2 (latest)", - Repository: model.Repository{ - URL: "https://github.com/example/filesystem", - Source: "github", - ID: "example/filesystem", - }, - Version: "2.0.0", - } - server2 := apiv0.ServerJSON{ - Name: "com.example/database-server", - Description: "Database operations server", - Repository: model.Repository{ - URL: "https://github.com/example/database", - Source: "github", - ID: "example/database", - }, - Version: "1.0.0", - } - server3 := apiv0.ServerJSON{ - Name: "org.another/filesystem-tools", - Description: "Filesystem tools and utilities", - Repository: model.Repository{ - URL: "https://github.com/another/filesystem-tools", - Source: "github", - ID: "another/filesystem-tools", - }, - Version: "3.0.0", - } - _, _ = registry.Publish(server1v1) - _, _ = registry.Publish(server1v2) - _, _ = registry.Publish(server2) - _, _ = registry.Publish(server3) - }, - expectedStatus: http.StatusOK, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create mock registry service - registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) - tc.setupRegistryService(registryService) - - // Create a new test API - mux := http.NewServeMux() - api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) - - // Register the servers endpoints - v0.RegisterServersEndpoints(api, registryService) - - // Create request - url := "/v0/servers" + tc.queryParams - req := httptest.NewRequest(http.MethodGet, url, nil) - w := httptest.NewRecorder() - - // Serve the request - mux.ServeHTTP(w, req) - - // Check status code - assert.Equal(t, tc.expectedStatus, w.Code) - - if tc.expectedStatus == http.StatusOK { - // Check content type - assert.Equal(t, "application/json", w.Header().Get("Content-Type")) - - // Parse response body - var resp apiv0.ServerListResponse - err := json.NewDecoder(w.Body).Decode(&resp) - assert.NoError(t, err) - - // Check the response data - switch tc.name { - case "successful search by name substring": - assert.Len(t, resp.Servers, 1, "Expected exactly one matching server") - assert.Contains(t, resp.Servers[0].Name, "test-server", "Server name should contain search term") - case "successful updated_since filter with RFC3339": - assert.Len(t, resp.Servers, 1, "Expected one server updated after 2020") - assert.Contains(t, resp.Servers[0].Name, "recent-server") - case "successful version=latest filter": - assert.Len(t, resp.Servers, 1, "Expected one latest server") - assert.Contains(t, resp.Servers[0].Description, "latest") - case "combined search and updated_since filter": - assert.Len(t, resp.Servers, 1, "Expected one server matching both filters") - assert.Contains(t, resp.Servers[0].Name, "combined", "Server name should contain search term") - case "empty registry returns success": - assert.Empty(t, resp.Servers, "Expected empty server list for empty registry") - case "comprehensive query with all parameters": - // Should return only latest versions of servers matching "filesystem" search term - // Expected: 2 servers (filesystem-server v2.0.0 and filesystem-tools v3.0.0) - assert.Len(t, resp.Servers, 2, "Expected two servers matching all filters") - for _, server := range resp.Servers { - assert.Contains(t, server.Name, "filesystem", "Server name should contain 'filesystem'") - } - // Verify the limit parameter worked (should be at most 50, but we only have 2) - assert.LessOrEqual(t, len(resp.Servers), 50, "Should respect limit parameter") - // Verify response includes metadata - assert.NotNil(t, resp.Metadata, "Expected metadata to be present") - assert.Equal(t, 2, resp.Metadata.Count, "Metadata count should match returned servers") - default: - assert.NotEmpty(t, resp.Servers, "Expected at least one server") - } - - // General structure validation - for _, server := range resp.Servers { - assert.NotEmpty(t, server.Name) - assert.NotEmpty(t, server.Description) - assert.NotNil(t, server.Meta) - assert.NotNil(t, server.Meta.Official) - assert.NotEmpty(t, server.Meta.Official.VersionID) - } - - // Check metadata if expected - if tc.expectedMeta != nil { - assert.NotNil(t, resp.Metadata, "Expected metadata to be present") - assert.Equal(t, tc.expectedMeta.Count, resp.Metadata.Count) - if tc.expectedMeta.NextCursor != "" { - assert.NotEmpty(t, resp.Metadata.NextCursor) - } - } - } else if tc.expectedError != "" { - // Check error message for non-200 responses - assert.Contains(t, w.Body.String(), tc.expectedError) - } - - // Verify mock expectations - // No expectations to verify with real service - }) - } -} - -func TestServersDetailEndpoint(t *testing.T) { - // Create mock registry service +func TestListServersEndpoint(t *testing.T) { + ctx := context.Background() registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) - // Publish multiple versions of the same server - testServer1, err := registryService.Publish(apiv0.ServerJSON{ - Name: "com.example/test-server", - Description: "A test server", + // Setup test data + _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Name: "com.example/server-alpha", + Description: "Alpha test server", Version: "1.0.0", }) - assert.NoError(t, err) + require.NoError(t, err) - _, err = registryService.Publish(apiv0.ServerJSON{ - Name: "com.example/test-server", - Description: "A test server updated", + _, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Name: "com.example/server-beta", + Description: "Beta test server", Version: "2.0.0", }) - assert.NoError(t, err) + require.NoError(t, err) - testCases := []struct { + // Create API + mux := http.NewServeMux() + api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) + v0.RegisterServersEndpoints(api, registryService) + + tests := []struct { name string - serverID string - version string - expectedStatus int - expectedServer *apiv0.ServerJSON - expectedError string - }{ - { - name: "successful get server detail (latest)", - serverID: testServer1.Meta.Official.ServerID, - expectedStatus: http.StatusOK, - }, - { - name: "successful get server detail with specific version", - serverID: testServer1.Meta.Official.ServerID, - version: "1.0.0", - expectedStatus: http.StatusOK, - }, - { - name: "successful get server detail with latest version", - serverID: testServer1.Meta.Official.ServerID, - version: "2.0.0", - expectedStatus: http.StatusOK, - }, - { - name: "version not found for server", - serverID: testServer1.Meta.Official.ServerID, - version: "3.0.0", - expectedStatus: http.StatusNotFound, - expectedError: "Server not found", - }, - { - name: "invalid server ID format", - serverID: "invalid-uuid", - expectedStatus: http.StatusUnprocessableEntity, - expectedError: "validation failed", - }, - { - name: "server not found", - serverID: uuid.New().String(), - expectedStatus: http.StatusNotFound, - expectedError: "Server not found", - }, - } - - 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 servers endpoints - v0.RegisterServersEndpoints(api, registryService) - - // Create request - url := "/v0/servers/" + tc.serverID - if tc.version != "" { - url += "?version=" + tc.version - } - req := httptest.NewRequest(http.MethodGet, url, nil) - w := httptest.NewRecorder() - - // Serve the request - mux.ServeHTTP(w, req) - - // Check status code - assert.Equal(t, tc.expectedStatus, w.Code) - - if tc.expectedStatus == http.StatusOK { - // Check content type - assert.Equal(t, "application/json", w.Header().Get("Content-Type")) - - // Parse response body - var serverDetailResp apiv0.ServerJSON - err := json.NewDecoder(w.Body).Decode(&serverDetailResp) - assert.NoError(t, err) - - // Check that we got a valid response - assert.NotEmpty(t, serverDetailResp.Name) - } else if tc.expectedError != "" { - // Check error message for non-200 responses - assert.Contains(t, w.Body.String(), tc.expectedError) - } - - // Verify mock expectations - // No expectations to verify with real service - }) - } -} - -func TestServersVersionsEndpoint(t *testing.T) { - // Create mock registry service - registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) - - // Publish multiple versions of the same server - testServer1, err := registryService.Publish(apiv0.ServerJSON{ - Name: "com.example/versioned-server", - Description: "A versioned test server", - Version: "1.0.0", - }) - assert.NoError(t, err) - - _, err = registryService.Publish(apiv0.ServerJSON{ - Name: "com.example/versioned-server", - Description: "A versioned test server updated", - Version: "2.0.0", - }) - assert.NoError(t, err) - - _, err = registryService.Publish(apiv0.ServerJSON{ - Name: "com.example/versioned-server", - Description: "A versioned test server latest", - Version: "2.1.0", - }) - assert.NoError(t, err) - - testCases := []struct { - name string - serverID string + queryParams string expectedStatus int expectedCount int expectedError string }{ { - name: "successful get all versions", - serverID: testServer1.Meta.Official.ServerID, + name: "list all servers", + queryParams: "", expectedStatus: http.StatusOK, - expectedCount: 3, + expectedCount: 2, }, { - name: "invalid server ID format", - serverID: "invalid-uuid", + name: "list with limit", + queryParams: "?limit=1", + expectedStatus: http.StatusOK, + expectedCount: 1, + }, + { + name: "search servers", + queryParams: "?search=alpha", + expectedStatus: http.StatusOK, + expectedCount: 1, + }, + { + name: "filter latest only", + queryParams: "?version=latest", + expectedStatus: http.StatusOK, + expectedCount: 2, + }, + { + name: "invalid limit", + queryParams: "?limit=abc", expectedStatus: http.StatusUnprocessableEntity, expectedError: "validation failed", }, - { - name: "server not found", - serverID: uuid.New().String(), - expectedStatus: http.StatusNotFound, - expectedError: "Server not found", - }, } - 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 servers endpoints - v0.RegisterServersEndpoints(api, registryService) - - // Create request - url := "/v0/servers/" + tc.serverID + "/versions" - req := httptest.NewRequest(http.MethodGet, url, nil) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/v0/servers"+tt.queryParams, nil) w := httptest.NewRecorder() - // Serve the request mux.ServeHTTP(w, req) - // Check status code - assert.Equal(t, tc.expectedStatus, w.Code) + assert.Equal(t, tt.expectedStatus, w.Code) - if tc.expectedStatus == http.StatusOK { - // Check content type - assert.Equal(t, "application/json", w.Header().Get("Content-Type")) - - // Parse response body - var versionsResp apiv0.ServerListResponse - err := json.NewDecoder(w.Body).Decode(&versionsResp) + if tt.expectedStatus == http.StatusOK { + var resp apiv0.ServerListResponse + err := json.NewDecoder(w.Body).Decode(&resp) assert.NoError(t, err) + assert.Len(t, resp.Servers, tt.expectedCount) + assert.Equal(t, tt.expectedCount, resp.Metadata.Count) - // Check the response data - assert.Len(t, versionsResp.Servers, tc.expectedCount) - assert.Equal(t, tc.expectedCount, versionsResp.Metadata.Count) - - // Verify all returned servers have the same server ID but different versions - for _, server := range versionsResp.Servers { - assert.Equal(t, tc.serverID, server.Meta.Official.ServerID) - assert.NotEmpty(t, server.Version) - assert.Equal(t, "com.example/versioned-server", server.Name) + // Verify structure + for _, server := range resp.Servers { + assert.NotEmpty(t, server.Server.Name) + assert.NotEmpty(t, server.Server.Description) + assert.NotNil(t, server.Meta.Official) } - - // Verify versions are included (should have 1.0.0, 2.0.0, 2.1.0) - versions := make([]string, 0, len(versionsResp.Servers)) - for _, server := range versionsResp.Servers { - versions = append(versions, server.Version) - } - assert.Contains(t, versions, "1.0.0") - assert.Contains(t, versions, "2.0.0") - assert.Contains(t, versions, "2.1.0") - } else if tc.expectedError != "" { - // Check error message for non-200 responses - assert.Contains(t, w.Body.String(), tc.expectedError) + } else if tt.expectedError != "" { + assert.Contains(t, w.Body.String(), tt.expectedError) } }) } } -// TestServersEndpointsIntegration tests the servers endpoints with actual HTTP requests -func TestServersEndpointsIntegration(t *testing.T) { - // Create mock registry service +func TestGetServerByNameEndpoint(t *testing.T) { + ctx := context.Background() registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) - // Test data - publish a server and get its actual ID - testServer := apiv0.ServerJSON{ - Name: "com.example/integration-test-server", - Description: "Integration test server", - Repository: model.Repository{ - URL: "https://github.com/example/integration-test", - Source: "github", - ID: "example/integration-test", - }, - Version: "1.0.0", - } + // Setup test data + _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Name: "com.example/detail-server", + Description: "Server for detail testing", + Version: "1.0.0", + }) + require.NoError(t, err) - published, err := registryService.Publish(testServer) - assert.NoError(t, err) - assert.NotNil(t, published) - - serverID := published.Meta.Official.ServerID - servers := []apiv0.ServerJSON{*published} - serverDetail := published - - // Create a new test API + // Create API mux := http.NewServeMux() api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) - - // Register the servers endpoints v0.RegisterServersEndpoints(api, registryService) - // Create test server - server := httptest.NewServer(mux) - defer server.Close() + tests := []struct { + name string + serverName string + expectedStatus int + expectedError string + }{ + { + name: "get existing server", + serverName: "com.example/detail-server", + expectedStatus: http.StatusOK, + }, + { + name: "get non-existent server", + serverName: "com.example/non-existent", + expectedStatus: http.StatusNotFound, + expectedError: "Server not found", + }, + } - // Test list endpoint - t.Run("list servers integration", func(t *testing.T) { - ctx := context.Background() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL+"/v0/servers", nil) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // URL encode the server name + encodedName := url.PathEscape(tt.serverName) + req := httptest.NewRequest(http.MethodGet, "/v0/servers/"+encodedName, nil) + w := httptest.NewRecorder() - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - defer resp.Body.Close() + mux.ServeHTTP(w, req) - // Check status code - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, tt.expectedStatus, w.Code) - // Check content type - assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) - - // Parse response body - var listResp apiv0.ServerListResponse - err = json.NewDecoder(resp.Body).Decode(&listResp) - assert.NoError(t, err) - - // Check the response data (excluding timestamps which will be different) - assert.Len(t, listResp.Servers, len(servers)) - if len(listResp.Servers) > 0 { - assert.Equal(t, servers[0].Name, listResp.Servers[0].Name) - assert.Equal(t, servers[0].Description, listResp.Servers[0].Description) - assert.Equal(t, servers[0].Repository, listResp.Servers[0].Repository) - assert.Equal(t, servers[0].Version, listResp.Servers[0].Version) - } - }) - - // Test get server detail endpoint - t.Run("get server detail integration", func(t *testing.T) { - ctx := context.Background() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL+"/v0/servers/"+serverID, nil) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - } - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - defer resp.Body.Close() - - // Check status code - assert.Equal(t, http.StatusOK, resp.StatusCode) - - // Check content type - assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) - - // Parse response body - var serverDetailResp apiv0.ServerJSON - err = json.NewDecoder(resp.Body).Decode(&serverDetailResp) - assert.NoError(t, err) - - // Check the response data (excluding timestamps which will be different) - assert.Equal(t, serverDetail.Name, serverDetailResp.Name) - assert.Equal(t, serverDetail.Description, serverDetailResp.Description) - assert.Equal(t, serverDetail.Repository, serverDetailResp.Repository) - assert.Equal(t, serverDetail.Version, serverDetailResp.Version) - }) - - // Verify mock expectations - // No expectations to verify with real service + if tt.expectedStatus == http.StatusOK { + var resp apiv0.ServerResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + assert.Equal(t, tt.serverName, resp.Server.Name) + assert.NotNil(t, resp.Meta.Official) + } else if tt.expectedError != "" { + assert.Contains(t, w.Body.String(), tt.expectedError) + } + }) + } } + +func TestGetServerVersionEndpoint(t *testing.T) { + ctx := context.Background() + registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) + + serverName := "com.example/version-server" + + // Setup test data with multiple versions + _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, + Description: "Version test server v1", + Version: "1.0.0", + }) + require.NoError(t, err) + + _, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, + Description: "Version test server v2", + Version: "2.0.0", + }) + require.NoError(t, err) + + // Add version with build metadata for URL encoding test + _, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, + Description: "Version test server with build metadata", + Version: "1.0.0+20130313144700", + }) + require.NoError(t, err) + + // Create API + mux := http.NewServeMux() + api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) + v0.RegisterServersEndpoints(api, registryService) + + tests := []struct { + name string + serverName string + version string + expectedStatus int + expectedError string + checkResult func(*testing.T, *apiv0.ServerResponse) + }{ + { + name: "get existing version", + serverName: serverName, + version: "1.0.0", + expectedStatus: http.StatusOK, + checkResult: func(t *testing.T, resp *apiv0.ServerResponse) { + t.Helper() + assert.Equal(t, "1.0.0", resp.Server.Version) + assert.Equal(t, "Version test server v1", resp.Server.Description) + assert.False(t, resp.Meta.Official.IsLatest) + }, + }, + { + name: "get latest version", + serverName: serverName, + version: "2.0.0", + expectedStatus: http.StatusOK, + checkResult: func(t *testing.T, resp *apiv0.ServerResponse) { + t.Helper() + assert.Equal(t, "2.0.0", resp.Server.Version) + assert.True(t, resp.Meta.Official.IsLatest) + }, + }, + { + name: "get non-existent version", + serverName: serverName, + version: "3.0.0", + expectedStatus: http.StatusNotFound, + expectedError: "Server not found", + }, + { + name: "get non-existent server", + serverName: "com.example/non-existent", + version: "1.0.0", + expectedStatus: http.StatusNotFound, + expectedError: "Server not found", + }, + { + name: "get version with build metadata (URL encoded)", + serverName: serverName, + version: "1.0.0+20130313144700", + expectedStatus: http.StatusOK, + checkResult: func(t *testing.T, resp *apiv0.ServerResponse) { + t.Helper() + assert.Equal(t, "1.0.0+20130313144700", resp.Server.Version) + assert.Equal(t, "Version test server with build metadata", resp.Server.Description) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // URL encode the server name and version + encodedName := url.PathEscape(tt.serverName) + encodedVersion := url.PathEscape(tt.version) + req := httptest.NewRequest(http.MethodGet, "/v0/servers/"+encodedName+"/versions/"+encodedVersion, nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + + if tt.expectedStatus == http.StatusOK { + var resp apiv0.ServerResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + assert.Equal(t, tt.serverName, resp.Server.Name) + assert.Equal(t, tt.version, resp.Server.Version) + assert.NotNil(t, resp.Meta.Official) + + if tt.checkResult != nil { + tt.checkResult(t, &resp) + } + } else if tt.expectedError != "" { + assert.Contains(t, w.Body.String(), tt.expectedError) + } + }) + } +} + +func TestGetAllVersionsEndpoint(t *testing.T) { + ctx := context.Background() + registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) + + serverName := "com.example/multi-version-server" + + // Setup test data with multiple versions + versions := []string{"1.0.0", "1.1.0", "2.0.0"} + for _, version := range versions { + _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, + Description: "Multi-version test server " + version, + Version: version, + }) + require.NoError(t, err) + } + + // Create API + mux := http.NewServeMux() + api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) + v0.RegisterServersEndpoints(api, registryService) + + tests := []struct { + name string + serverName string + expectedStatus int + expectedCount int + expectedError string + }{ + { + name: "get all versions of existing server", + serverName: serverName, + expectedStatus: http.StatusOK, + expectedCount: 3, + }, + { + name: "get versions of non-existent server", + serverName: "com.example/non-existent", + expectedStatus: http.StatusNotFound, + expectedError: "Server not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // URL encode the server name + encodedName := url.PathEscape(tt.serverName) + req := httptest.NewRequest(http.MethodGet, "/v0/servers/"+encodedName+"/versions", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + + if tt.expectedStatus == http.StatusOK { + var resp apiv0.ServerListResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + assert.Len(t, resp.Servers, tt.expectedCount) + assert.Equal(t, tt.expectedCount, resp.Metadata.Count) + + // Verify all versions are for the same server + for _, server := range resp.Servers { + assert.Equal(t, tt.serverName, server.Server.Name) + assert.NotNil(t, server.Meta.Official) + } + + // Verify all expected versions are present + versionSet := make(map[string]bool) + for _, server := range resp.Servers { + versionSet[server.Server.Version] = true + } + for _, expectedVersion := range versions { + assert.True(t, versionSet[expectedVersion], "Version %s should be present", expectedVersion) + } + + // Verify exactly one is marked as latest + latestCount := 0 + for _, server := range resp.Servers { + if server.Meta.Official.IsLatest { + latestCount++ + } + } + assert.Equal(t, 1, latestCount, "Exactly one version should be marked as latest") + } else if tt.expectedError != "" { + assert.Contains(t, w.Body.String(), tt.expectedError) + } + }) + } +} + +func TestServersEndpointEdgeCases(t *testing.T) { + ctx := context.Background() + registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) + + // Setup test data with edge case names that comply with constraints + specialServers := []struct { + name string + description string + version string + }{ + {"io.dots.and-dashes/server-name", "Server with dots and dashes", "1.0.0"}, + {"com.long-namespace-name/very-long-server-name-here", "Long names", "1.0.0"}, + {"org.test123/server_with_underscores", "Server with underscores", "1.0.0"}, + } + + for _, server := range specialServers { + _, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{ + Name: server.name, + Description: server.description, + Version: server.version, + }) + require.NoError(t, err) + } + + // Create API + mux := http.NewServeMux() + api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0")) + v0.RegisterServersEndpoints(api, registryService) + + t.Run("URL encoding edge cases", func(t *testing.T) { + tests := []struct { + name string + serverName string + }{ + {"dots and dashes", "io.dots.and-dashes/server-name"}, + {"long server name", "com.long-namespace-name/very-long-server-name-here"}, + {"underscores", "org.test123/server_with_underscores"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test server detail endpoint + encodedName := url.PathEscape(tt.serverName) + req := httptest.NewRequest(http.MethodGet, "/v0/servers/"+encodedName, nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp apiv0.ServerResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + assert.Equal(t, tt.serverName, resp.Server.Name) + }) + } + }) + + t.Run("query parameter edge cases", func(t *testing.T) { + tests := []struct { + name string + queryParams string + expectedStatus int + expectedError string + }{ + {"limit too high", "?limit=1000", http.StatusUnprocessableEntity, "validation failed"}, + {"negative limit", "?limit=-1", http.StatusUnprocessableEntity, "validation failed"}, + {"invalid updated_since format", "?updated_since=invalid", http.StatusBadRequest, "Invalid updated_since format"}, + {"future updated_since", "?updated_since=2030-01-01T00:00:00Z", http.StatusOK, ""}, + {"very old updated_since", "?updated_since=1990-01-01T00:00:00Z", http.StatusOK, ""}, + {"empty search parameter", "?search=", http.StatusOK, ""}, + {"search with special characters", "?search=测试", http.StatusOK, ""}, + {"combined valid parameters", "?search=server&limit=5&version=latest", http.StatusOK, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/v0/servers"+tt.queryParams, nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + + if tt.expectedStatus == http.StatusOK { + var resp apiv0.ServerListResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Metadata) + } else if tt.expectedError != "" { + assert.Contains(t, w.Body.String(), tt.expectedError) + } + }) + } + }) + + t.Run("response structure validation", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/v0/servers", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var resp apiv0.ServerListResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + + // Verify metadata structure + assert.NotNil(t, resp.Metadata) + assert.GreaterOrEqual(t, resp.Metadata.Count, 0) + + // Verify each server has complete structure + for _, server := range resp.Servers { + assert.NotEmpty(t, server.Server.Name) + assert.NotEmpty(t, server.Server.Description) + assert.NotEmpty(t, server.Server.Version) + assert.NotNil(t, server.Meta) + assert.NotNil(t, server.Meta.Official) + assert.NotZero(t, server.Meta.Official.PublishedAt) + assert.Contains(t, []model.Status{model.StatusActive, model.StatusDeprecated, model.StatusDeleted}, server.Meta.Official.Status) + } + }) +} \ No newline at end of file diff --git a/internal/api/handlers/v0/telemetry_test.go b/internal/api/handlers/v0/telemetry_test.go index 795e3b1..2cffce1 100644 --- a/internal/api/handlers/v0/telemetry_test.go +++ b/internal/api/handlers/v0/telemetry_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/danielgtaylor/huma/v2" @@ -22,7 +23,7 @@ import ( func TestPrometheusHandler(t *testing.T) { registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig()) - server, err := registryService.Publish(apiv0.ServerJSON{ + server, err := registryService.CreateServer(context.Background(), &apiv0.ServerJSON{ Name: "io.github.example/test-server", Description: "Test server detail", Repository: model.Repository{ @@ -50,8 +51,8 @@ func TestPrometheusHandler(t *testing.T) { // Add /metrics for Prometheus metrics using promhttp mux.Handle("/metrics", metrics.PrometheusHandler()) - // Create request - url := "/v0/servers/" + server.Meta.Official.ServerID + // Create request - using new name-based API + url := "/v0/servers/" + url.PathEscape(server.Server.Name) req := httptest.NewRequest(http.MethodGet, url, nil) w := httptest.NewRecorder() @@ -76,5 +77,5 @@ func TestPrometheusHandler(t *testing.T) { // Check if the response body contains expected metrics assert.Contains(t, body, "mcp_registry_http_request_duration_bucket") assert.Contains(t, body, "mcp_registry_http_requests_total") - assert.Contains(t, body, "path=\"/v0/servers/{server_id}\"") + assert.Contains(t, body, "path=\"/v0/servers/{serverName}\"") } diff --git a/internal/database/database.go b/internal/database/database.go index d3d076e..ebae55d 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -31,20 +31,28 @@ type ServerFilter struct { // Database defines the interface for database operations type Database interface { - // Retrieve server entries with optional filtering - List(ctx context.Context, tx pgx.Tx, filter *ServerFilter, cursor string, limit int) ([]*apiv0.ServerJSON, string, error) - // Retrieve a single server by its version ID - GetByVersionID(ctx context.Context, tx pgx.Tx, versionID string) (*apiv0.ServerJSON, error) - // Retrieve latest version of a server by server ID - GetByServerID(ctx context.Context, tx pgx.Tx, serverID string) (*apiv0.ServerJSON, error) - // Retrieve specific version of a server by server ID and version - GetByServerIDAndVersion(ctx context.Context, tx pgx.Tx, serverID string, version string) (*apiv0.ServerJSON, error) - // Retrieve all versions of a server by server ID - GetAllVersionsByServerID(ctx context.Context, tx pgx.Tx, serverID string) ([]*apiv0.ServerJSON, error) - // CreateServer inserts a new server version - CreateServer(ctx context.Context, tx pgx.Tx, newServer *apiv0.ServerJSON) (*apiv0.ServerJSON, error) + // CreateServer inserts a new server version with official metadata + CreateServer(ctx context.Context, tx pgx.Tx, serverJSON *apiv0.ServerJSON, officialMeta *apiv0.RegistryExtensions) (*apiv0.ServerResponse, error) // UpdateServer updates an existing server record - UpdateServer(ctx context.Context, tx pgx.Tx, id string, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) + UpdateServer(ctx context.Context, tx pgx.Tx, serverName, version string, serverJSON *apiv0.ServerJSON) (*apiv0.ServerResponse, error) + // SetServerStatus updates the status of a specific server version + SetServerStatus(ctx context.Context, tx pgx.Tx, serverName, version string, status string) (*apiv0.ServerResponse, error) + // ListServers retrieve server entries with optional filtering + ListServers(ctx context.Context, tx pgx.Tx, filter *ServerFilter, cursor string, limit int) ([]*apiv0.ServerResponse, string, error) + // GetServerByName retrieve a single server by its name + GetServerByName(ctx context.Context, tx pgx.Tx, serverName string) (*apiv0.ServerResponse, error) + // GetServerByNameAndVersion retrieve specific version of a server by server name and version + GetServerByNameAndVersion(ctx context.Context, tx pgx.Tx, serverName string, version string) (*apiv0.ServerResponse, error) + // GetAllVersionsByServerName retrieve all versions of a server by server name + GetAllVersionsByServerName(ctx context.Context, tx pgx.Tx, serverName string) ([]*apiv0.ServerResponse, error) + // GetCurrentLatestVersion retrieve the current latest version of a server by server name + GetCurrentLatestVersion(ctx context.Context, tx pgx.Tx, serverName string) (*apiv0.ServerResponse, error) + // CountServerVersions count the number of versions for a server + CountServerVersions(ctx context.Context, tx pgx.Tx, serverName string) (int, error) + // CheckVersionExists check if a specific version exists for a server + CheckVersionExists(ctx context.Context, tx pgx.Tx, serverName, version string) (bool, error) + // UnmarkAsLatest marks the current latest version of a server as no longer latest + UnmarkAsLatest(ctx context.Context, tx pgx.Tx, serverName string) error // AcquirePublishLock acquires an exclusive advisory lock for publishing a server // This prevents race conditions when multiple versions are published concurrently AcquirePublishLock(ctx context.Context, tx pgx.Tx, serverName string) error diff --git a/internal/database/migrations/008_separate_official_metadata.sql b/internal/database/migrations/008_separate_official_metadata.sql new file mode 100644 index 0000000..5bafe17 --- /dev/null +++ b/internal/database/migrations/008_separate_official_metadata.sql @@ -0,0 +1,143 @@ +-- Separate official metadata from JSON to dedicated columns for better performance and consistency +-- This migration transforms the database from JSON-heavy to columnar structure for registry-managed data + +BEGIN; + +-- Add new columns for official metadata and core identifiers +ALTER TABLE servers ADD COLUMN server_name VARCHAR(255); +ALTER TABLE servers ADD COLUMN version VARCHAR(255); +ALTER TABLE servers ADD COLUMN status VARCHAR(50); +ALTER TABLE servers ADD COLUMN published_at TIMESTAMP WITH TIME ZONE; +ALTER TABLE servers ADD COLUMN updated_at TIMESTAMP WITH TIME ZONE; +ALTER TABLE servers ADD COLUMN is_latest BOOLEAN; + +-- Create a function to migrate existing data from JSON to columns +CREATE OR REPLACE FUNCTION migrate_official_metadata() +RETURNS VOID AS $$ +DECLARE + rec RECORD; + official_meta JSONB; + updated_value JSONB; + server_meta JSONB; + publisher_provided JSONB; +BEGIN + -- Iterate through all existing records + FOR rec IN SELECT version_id, value FROM servers ORDER BY version_id LOOP + -- Extract core identifiers + UPDATE servers + SET + server_name = rec.value->>'name', + version = rec.value->>'version' + WHERE version_id = rec.version_id; + + -- Extract official metadata from nested JSON structure + official_meta := rec.value->'_meta'->'io.modelcontextprotocol.registry/official'; + + IF official_meta IS NOT NULL THEN + -- Update columns with extracted metadata + UPDATE servers + SET + status = (official_meta->>'status')::VARCHAR(50), + published_at = (official_meta->>'publishedAt')::TIMESTAMP WITH TIME ZONE, + updated_at = (official_meta->>'updatedAt')::TIMESTAMP WITH TIME ZONE, + is_latest = (official_meta->>'isLatest')::BOOLEAN + WHERE version_id = rec.version_id; + ELSE + -- Handle records without official metadata (set defaults) + UPDATE servers + SET + status = COALESCE((rec.value->>'status'), 'active'), + published_at = NOW(), + updated_at = NOW(), + is_latest = true + WHERE version_id = rec.version_id; + END IF; + + -- Clean up the JSON: remove status field and official metadata + updated_value := rec.value - 'status'; + + -- Reconstruct _meta with only publisher-provided data + publisher_provided := rec.value->'_meta'->'io.modelcontextprotocol.registry/publisher-provided'; + + IF publisher_provided IS NOT NULL THEN + server_meta := jsonb_build_object('io.modelcontextprotocol.registry/publisher-provided', publisher_provided); + updated_value := jsonb_set(updated_value, '{_meta}', server_meta); + ELSE + -- Remove _meta entirely if no publisher-provided data + updated_value := updated_value - '_meta'; + END IF; + + -- Update the JSON with cleaned structure (keeping name and version for immutable server.json) + UPDATE servers + SET value = updated_value + WHERE version_id = rec.version_id; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +-- Execute the migration +SELECT migrate_official_metadata(); + +-- Drop the migration function +DROP FUNCTION migrate_official_metadata(); + +-- Make the new columns NOT NULL now that all records have values +ALTER TABLE servers ALTER COLUMN server_name SET NOT NULL; +ALTER TABLE servers ALTER COLUMN version SET NOT NULL; +ALTER TABLE servers ALTER COLUMN status SET NOT NULL; +ALTER TABLE servers ALTER COLUMN published_at SET NOT NULL; +ALTER TABLE servers ALTER COLUMN is_latest SET NOT NULL; + +-- Drop the old primary key constraint +ALTER TABLE servers DROP CONSTRAINT servers_pkey; + +-- Create new composite primary key using name + version (natural key) +ALTER TABLE servers ADD CONSTRAINT servers_pkey PRIMARY KEY (server_name, version); + +-- Drop the old version_id column since we're using name-based keys now +ALTER TABLE servers DROP COLUMN version_id; + +-- Drop old indexes that used JSON paths and UUIDs +DROP INDEX IF EXISTS idx_servers_version_id; +DROP INDEX IF EXISTS idx_servers_name_latest; +DROP INDEX IF EXISTS idx_servers_updated_at; +DROP INDEX IF EXISTS idx_servers_remotes; +DROP INDEX IF EXISTS idx_unique_server_version; +DROP INDEX IF EXISTS idx_unique_latest_version; + +-- Create new efficient indexes on the dedicated columns +CREATE INDEX idx_servers_name ON servers (server_name); +CREATE INDEX idx_servers_name_version ON servers (server_name, version); +CREATE INDEX idx_servers_name_latest ON servers (server_name, is_latest) WHERE is_latest = true; +CREATE INDEX idx_servers_status ON servers (status); +CREATE INDEX idx_servers_published_at ON servers (published_at DESC); +CREATE INDEX idx_servers_updated_at ON servers (updated_at DESC) WHERE updated_at IS NOT NULL; + +-- Ensure only one version per server can be marked as latest +CREATE UNIQUE INDEX idx_unique_latest_per_server +ON servers (server_name) +WHERE is_latest = true; + +-- Create GIN index for remaining JSON queries (remotes, packages, etc.) +CREATE INDEX idx_servers_json_remotes ON servers USING GIN((value->'remotes')); +CREATE INDEX idx_servers_json_packages ON servers USING GIN((value->'packages')); + +-- Update $schema field to latest version for all entries +UPDATE servers +SET value = jsonb_set(value, '{$schema}', '"https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json"') +WHERE value ? '$schema' AND value IS NOT NULL; + +-- Add check constraints for data integrity +ALTER TABLE servers ADD CONSTRAINT check_status_valid +CHECK (status IN ('active', 'deprecated', 'deleted')); + +ALTER TABLE servers ADD CONSTRAINT check_server_name_format +CHECK (server_name ~ '^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]/[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$'); + +ALTER TABLE servers ADD CONSTRAINT check_version_not_empty +CHECK (length(trim(version)) > 0); + +ALTER TABLE servers ADD CONSTRAINT check_published_at_reasonable +CHECK (published_at >= '2020-01-01'::timestamp AND published_at <= NOW() + interval '1 day'); + +COMMIT; \ No newline at end of file diff --git a/internal/database/postgres.go b/internal/database/postgres.go index 450810b..b47ef74 100644 --- a/internal/database/postgres.go +++ b/internal/database/postgres.go @@ -9,12 +9,12 @@ import ( "strings" "time" - "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" ) // PostgreSQL is an implementation of the Database interface using PostgreSQL @@ -79,14 +79,13 @@ func NewPostgreSQL(ctx context.Context, connectionURI string) (*PostgreSQL, erro }, nil } -//nolint:cyclop // Database filtering logic is inherently complex but clear -func (db *PostgreSQL) List( +func (db *PostgreSQL) ListServers( ctx context.Context, tx pgx.Tx, filter *ServerFilter, cursor string, limit int, -) ([]*apiv0.ServerJSON, string, error) { +) ([]*apiv0.ServerResponse, string, error) { if limit <= 0 { limit = 10 } @@ -95,15 +94,15 @@ func (db *PostgreSQL) List( return nil, "", ctx.Err() } - // Build WHERE clause for filtering + // Build WHERE clause for filtering using dedicated columns var whereConditions []string args := []any{} argIndex := 1 - // Add filters using JSON operators + // Add filters using dedicated columns for better performance if filter != nil { if filter.Name != nil { - whereConditions = append(whereConditions, fmt.Sprintf("value->>'name' = $%d", argIndex)) + whereConditions = append(whereConditions, fmt.Sprintf("server_name = $%d", argIndex)) args = append(args, *filter.Name) argIndex++ } @@ -113,35 +112,45 @@ func (db *PostgreSQL) List( argIndex++ } if filter.UpdatedSince != nil { - whereConditions = append(whereConditions, fmt.Sprintf("(value->'_meta'->'io.modelcontextprotocol.registry/official'->>'updatedAt')::timestamp > $%d", argIndex)) + whereConditions = append(whereConditions, fmt.Sprintf("updated_at > $%d", argIndex)) args = append(args, *filter.UpdatedSince) argIndex++ } if filter.SubstringName != nil { - whereConditions = append(whereConditions, fmt.Sprintf("value->>'name' ILIKE $%d", argIndex)) + whereConditions = append(whereConditions, fmt.Sprintf("server_name ILIKE $%d", argIndex)) args = append(args, "%"+*filter.SubstringName+"%") argIndex++ } if filter.Version != nil { - whereConditions = append(whereConditions, fmt.Sprintf("(value->'version_detail'->>'version') = $%d", argIndex)) + whereConditions = append(whereConditions, fmt.Sprintf("version = $%d", argIndex)) args = append(args, *filter.Version) argIndex++ } if filter.IsLatest != nil { - whereConditions = append(whereConditions, fmt.Sprintf("(value->'_meta'->'io.modelcontextprotocol.registry/official'->>'isLatest')::boolean = $%d", argIndex)) + whereConditions = append(whereConditions, fmt.Sprintf("is_latest = $%d", argIndex)) args = append(args, *filter.IsLatest) argIndex++ } } - // Add cursor pagination using primary key version_id + // Add cursor pagination using compound serverName:version cursor if cursor != "" { - if _, err := uuid.Parse(cursor); err != nil { - return nil, "", fmt.Errorf("invalid cursor format: %w", err) + // Parse cursor format: "serverName:version" + parts := strings.SplitN(cursor, ":", 2) + if len(parts) == 2 { + cursorServerName := parts[0] + cursorVersion := parts[1] + + // Use compound condition: (server_name > cursor_name) OR (server_name = cursor_name AND version > cursor_version) + whereConditions = append(whereConditions, fmt.Sprintf("(server_name > $%d OR (server_name = $%d AND version > $%d))", argIndex, argIndex+1, argIndex+2)) + args = append(args, cursorServerName, cursorServerName, cursorVersion) + argIndex += 3 + } else { + // Fallback for malformed cursor - treat as server name only for backwards compatibility + whereConditions = append(whereConditions, fmt.Sprintf("server_name > $%d", argIndex)) + args = append(args, cursor) + argIndex++ } - whereConditions = append(whereConditions, fmt.Sprintf("version_id > $%d", argIndex)) - args = append(args, cursor) - argIndex++ } // Build the WHERE clause @@ -150,12 +159,12 @@ func (db *PostgreSQL) List( whereClause = "WHERE " + strings.Join(whereConditions, " AND ") } - // Simple query on servers table + // Query servers table with hybrid column/JSON data query := fmt.Sprintf(` - SELECT value + SELECT server_name, version, status, published_at, updated_at, is_latest, value FROM servers %s - ORDER BY version_id + ORDER BY server_name, version LIMIT $%d `, whereClause, argIndex) args = append(args, limit) @@ -166,170 +175,202 @@ func (db *PostgreSQL) List( } defer rows.Close() - var results []*apiv0.ServerJSON + var results []*apiv0.ServerResponse for rows.Next() { + var serverName, version, status string + var publishedAt, updatedAt time.Time + var isLatest bool var valueJSON []byte - err := rows.Scan(&valueJSON) + err := rows.Scan(&serverName, &version, &status, &publishedAt, &updatedAt, &isLatest, &valueJSON) if err != nil { return nil, "", fmt.Errorf("failed to scan server row: %w", err) } - // Parse the complete ServerJSON from JSONB + // Parse the ServerJSON from JSONB var serverJSON apiv0.ServerJSON if err := json.Unmarshal(valueJSON, &serverJSON); err != nil { return nil, "", fmt.Errorf("failed to unmarshal server JSON: %w", err) } - results = append(results, &serverJSON) + // Build ServerResponse with separated metadata + serverResponse := &apiv0.ServerResponse{ + Server: serverJSON, + Meta: apiv0.ResponseMeta{ + Official: &apiv0.RegistryExtensions{ + Status: model.Status(status), + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + IsLatest: isLatest, + }, + }, + } + + results = append(results, serverResponse) } if err := rows.Err(); err != nil { return nil, "", fmt.Errorf("error iterating rows: %w", err) } - // Determine next cursor using registry metadata VersionID + // Determine next cursor using compound serverName:version format nextCursor := "" if len(results) > 0 && len(results) >= limit { lastResult := results[len(results)-1] - if lastResult.Meta != nil && lastResult.Meta.Official != nil { - nextCursor = lastResult.Meta.Official.VersionID - } + nextCursor = lastResult.Server.Name + ":" + lastResult.Server.Version } return results, nextCursor, nil } -func (db *PostgreSQL) GetByVersionID(ctx context.Context, tx pgx.Tx, versionID string) (*apiv0.ServerJSON, error) { +// GetServerByName retrieves the latest version of a server by server name +func (db *PostgreSQL) GetServerByName(ctx context.Context, tx pgx.Tx, serverName string) (*apiv0.ServerResponse, error) { if ctx.Err() != nil { return nil, ctx.Err() } query := ` - SELECT value + SELECT server_name, version, status, published_at, updated_at, is_latest, value FROM servers - WHERE version_id = $1 - ` - - var valueJSON []byte - err := db.getExecutor(tx).QueryRow(ctx, query, versionID).Scan(&valueJSON) - - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrNotFound - } - return nil, fmt.Errorf("failed to get server by ID: %w", err) - } - - // Parse the complete ServerJSON from JSONB - var serverJSON apiv0.ServerJSON - if err := json.Unmarshal(valueJSON, &serverJSON); err != nil { - return nil, fmt.Errorf("failed to unmarshal server JSON: %w", err) - } - - return &serverJSON, nil -} - -// GetByServerID retrieves the latest version of a server by server ID -func (db *PostgreSQL) GetByServerID(ctx context.Context, tx pgx.Tx, serverID string) (*apiv0.ServerJSON, error) { - if ctx.Err() != nil { - return nil, ctx.Err() - } - - query := ` - SELECT value - FROM servers - WHERE (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'serverId') = $1 AND (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'isLatest')::boolean = true - ORDER BY (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'publishedAt')::timestamp DESC + WHERE server_name = $1 AND is_latest = true + ORDER BY published_at DESC LIMIT 1 ` + var name, version, status string + var publishedAt, updatedAt time.Time + var isLatest bool var valueJSON []byte - err := db.getExecutor(tx).QueryRow(ctx, query, serverID).Scan(&valueJSON) + err := db.getExecutor(tx).QueryRow(ctx, query, serverName).Scan(&name, &version, &status, &publishedAt, &updatedAt, &isLatest, &valueJSON) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } - return nil, fmt.Errorf("failed to get server by server ID: %w", err) + return nil, fmt.Errorf("failed to get server by name: %w", err) } - // Parse the complete ServerJSON from JSONB + // Parse the ServerJSON from JSONB var serverJSON apiv0.ServerJSON if err := json.Unmarshal(valueJSON, &serverJSON); err != nil { return nil, fmt.Errorf("failed to unmarshal server JSON: %w", err) } - return &serverJSON, nil + // Build ServerResponse with separated metadata + serverResponse := &apiv0.ServerResponse{ + Server: serverJSON, + Meta: apiv0.ResponseMeta{ + Official: &apiv0.RegistryExtensions{ + Status: model.Status(status), + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + IsLatest: isLatest, + }, + }, + } + + return serverResponse, nil } -// GetByServerIDAndVersion retrieves a specific version of a server by server ID and version -func (db *PostgreSQL) GetByServerIDAndVersion(ctx context.Context, tx pgx.Tx, serverID string, version string) (*apiv0.ServerJSON, error) { +// GetServerByNameAndVersion retrieves a specific version of a server by server name and version +func (db *PostgreSQL) GetServerByNameAndVersion(ctx context.Context, tx pgx.Tx, serverName string, version string) (*apiv0.ServerResponse, error) { if ctx.Err() != nil { return nil, ctx.Err() } query := ` - SELECT value + SELECT server_name, version, status, published_at, updated_at, is_latest, value FROM servers - WHERE (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'serverId') = $1 AND value->>'version' = $2 + WHERE server_name = $1 AND version = $2 LIMIT 1 ` + var name, vers, status string + var publishedAt, updatedAt time.Time + var isLatest bool var valueJSON []byte - err := db.getExecutor(tx).QueryRow(ctx, query, serverID, version).Scan(&valueJSON) + err := db.getExecutor(tx).QueryRow(ctx, query, serverName, version).Scan(&name, &vers, &status, &publishedAt, &updatedAt, &isLatest, &valueJSON) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } - return nil, fmt.Errorf("failed to get server by server ID and version: %w", err) + return nil, fmt.Errorf("failed to get server by name and version: %w", err) } - // Parse the complete ServerJSON from JSONB + // Parse the ServerJSON from JSONB var serverJSON apiv0.ServerJSON if err := json.Unmarshal(valueJSON, &serverJSON); err != nil { return nil, fmt.Errorf("failed to unmarshal server JSON: %w", err) } - return &serverJSON, nil + // Build ServerResponse with separated metadata + serverResponse := &apiv0.ServerResponse{ + Server: serverJSON, + Meta: apiv0.ResponseMeta{ + Official: &apiv0.RegistryExtensions{ + Status: model.Status(status), + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + IsLatest: isLatest, + }, + }, + } + + return serverResponse, nil } -// GetAllVersionsByServerID retrieves all versions of a server by server ID -func (db *PostgreSQL) GetAllVersionsByServerID(ctx context.Context, tx pgx.Tx, serverID string) ([]*apiv0.ServerJSON, error) { +// GetAllVersionsByServerName retrieves all versions of a server by server name +func (db *PostgreSQL) GetAllVersionsByServerName(ctx context.Context, tx pgx.Tx, serverName string) ([]*apiv0.ServerResponse, error) { if ctx.Err() != nil { return nil, ctx.Err() } query := ` - SELECT value + SELECT server_name, version, status, published_at, updated_at, is_latest, value FROM servers - WHERE (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'serverId') = $1 - ORDER BY (value->'_meta'->'io.modelcontextprotocol.registry/official'->>'publishedAt')::timestamp DESC + WHERE server_name = $1 + ORDER BY published_at DESC ` - rows, err := db.getExecutor(tx).Query(ctx, query, serverID) + rows, err := db.getExecutor(tx).Query(ctx, query, serverName) if err != nil { return nil, fmt.Errorf("failed to query server versions: %w", err) } defer rows.Close() - var results []*apiv0.ServerJSON + var results []*apiv0.ServerResponse for rows.Next() { + var name, version, status string + var publishedAt, updatedAt time.Time + var isLatest bool var valueJSON []byte - err := rows.Scan(&valueJSON) + err := rows.Scan(&name, &version, &status, &publishedAt, &updatedAt, &isLatest, &valueJSON) if err != nil { return nil, fmt.Errorf("failed to scan server row: %w", err) } - // Parse the complete ServerJSON from JSONB + // Parse the ServerJSON from JSONB var serverJSON apiv0.ServerJSON if err := json.Unmarshal(valueJSON, &serverJSON); err != nil { return nil, fmt.Errorf("failed to unmarshal server JSON: %w", err) } - results = append(results, &serverJSON) + // Build ServerResponse with separated metadata + serverResponse := &apiv0.ServerResponse{ + Server: serverJSON, + Meta: apiv0.ResponseMeta{ + Official: &apiv0.RegistryExtensions{ + Status: model.Status(status), + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + IsLatest: isLatest, + }, + }, + } + + results = append(results, serverResponse) } if err := rows.Err(); err != nil { @@ -343,77 +384,163 @@ func (db *PostgreSQL) GetAllVersionsByServerID(ctx context.Context, tx pgx.Tx, s return results, nil } -// CreateServer inserts a new server version -func (db *PostgreSQL) CreateServer(ctx context.Context, tx pgx.Tx, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) { +// CreateServer inserts a new server version with official metadata +func (db *PostgreSQL) CreateServer(ctx context.Context, tx pgx.Tx, serverJSON *apiv0.ServerJSON, officialMeta *apiv0.RegistryExtensions) (*apiv0.ServerResponse, error) { if ctx.Err() != nil { return nil, ctx.Err() } - // Get the IDs from the registry metadata - if server.Meta == nil || server.Meta.Official == nil { - return nil, fmt.Errorf("server must have registry metadata with ServerID and VersionID") + // Validate inputs + if serverJSON == nil || officialMeta == nil { + return nil, fmt.Errorf("serverJSON and officialMeta are required") } - versionID := server.Meta.Official.VersionID - if versionID == "" { - return nil, fmt.Errorf("server must have VersionID in registry metadata") + if serverJSON.Name == "" || serverJSON.Version == "" { + return nil, fmt.Errorf("server name and version are required") } - // Marshal the complete server to JSONB - valueJSON, err := json.Marshal(server) + // Marshal the ServerJSON to JSONB + valueJSON, err := json.Marshal(serverJSON) if err != nil { return nil, fmt.Errorf("failed to marshal server JSON: %w", err) } - // Insert the new version + // Insert the new server version using composite primary key insertQuery := ` - INSERT INTO servers (version_id, value) - VALUES ($1, $2) + INSERT INTO servers (server_name, version, status, published_at, updated_at, is_latest, value) + VALUES ($1, $2, $3, $4, $5, $6, $7) ` - _, err = db.getExecutor(tx).Exec(ctx, insertQuery, versionID, valueJSON) + _, err = db.getExecutor(tx).Exec(ctx, insertQuery, + serverJSON.Name, + serverJSON.Version, + string(officialMeta.Status), + officialMeta.PublishedAt, + officialMeta.UpdatedAt, + officialMeta.IsLatest, + valueJSON, + ) if err != nil { return nil, fmt.Errorf("failed to insert server: %w", err) } - return server, nil + // Return the complete ServerResponse + serverResponse := &apiv0.ServerResponse{ + Server: *serverJSON, + Meta: apiv0.ResponseMeta{ + Official: officialMeta, + }, + } + + return serverResponse, nil } // UpdateServer updates an existing server record with new server details -func (db *PostgreSQL) UpdateServer(ctx context.Context, tx pgx.Tx, id string, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) { +func (db *PostgreSQL) UpdateServer(ctx context.Context, tx pgx.Tx, serverName, version string, serverJSON *apiv0.ServerJSON) (*apiv0.ServerResponse, error) { if ctx.Err() != nil { return nil, ctx.Err() } - // Validate that meta structure exists and VersionID matches path - if server.Meta == nil || server.Meta.Official == nil || server.Meta.Official.VersionID != id { - return nil, fmt.Errorf("%w: io.modelcontextprotocol.registry/official.version_id must match path id (%s)", ErrInvalidInput, id) + // Validate inputs + if serverJSON == nil { + return nil, fmt.Errorf("serverJSON is required") } - // Marshal updated server - valueJSON, err := json.Marshal(server) + // Ensure the serverJSON matches the provided serverName and version + if serverJSON.Name != serverName || serverJSON.Version != version { + return nil, fmt.Errorf("%w: server name and version in JSON must match parameters", ErrInvalidInput) + } + + // Marshal updated ServerJSON + valueJSON, err := json.Marshal(serverJSON) if err != nil { return nil, fmt.Errorf("failed to marshal updated server: %w", err) } - // Update the complete server record using version_id + // Update only the JSON data (keep existing metadata columns) query := ` - UPDATE servers - SET value = $1 - WHERE version_id = $2 + UPDATE servers + SET value = $1, updated_at = NOW() + WHERE server_name = $2 AND version = $3 + RETURNING server_name, version, status, published_at, updated_at, is_latest ` - result, err := db.getExecutor(tx).Exec(ctx, query, valueJSON, id) + var name, vers, status string + var publishedAt, updatedAt time.Time + var isLatest bool + + err = db.getExecutor(tx).QueryRow(ctx, query, valueJSON, serverName, version).Scan(&name, &vers, &status, &publishedAt, &updatedAt, &isLatest) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrNotFound + } return nil, fmt.Errorf("failed to update server: %w", err) } - if result.RowsAffected() == 0 { - return nil, ErrNotFound + // Return the updated ServerResponse + serverResponse := &apiv0.ServerResponse{ + Server: *serverJSON, + Meta: apiv0.ResponseMeta{ + Official: &apiv0.RegistryExtensions{ + Status: model.Status(status), + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + IsLatest: isLatest, + }, + }, } - return server, nil + return serverResponse, nil +} + +// SetServerStatus updates the status of a specific server version +func (db *PostgreSQL) SetServerStatus(ctx context.Context, tx pgx.Tx, serverName, version string, status string) (*apiv0.ServerResponse, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + // Update the status column + query := ` + UPDATE servers + SET status = $1, updated_at = NOW() + WHERE server_name = $2 AND version = $3 + RETURNING server_name, version, status, value, published_at, updated_at, is_latest + ` + + var name, vers, currentStatus string + var publishedAt, updatedAt time.Time + var isLatest bool + var valueJSON []byte + + err := db.getExecutor(tx).QueryRow(ctx, query, status, serverName, version).Scan(&name, &vers, ¤tStatus, &valueJSON, &publishedAt, &updatedAt, &isLatest) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("failed to update server status: %w", err) + } + + // Unmarshal the JSON data + var serverJSON apiv0.ServerJSON + if err := json.Unmarshal(valueJSON, &serverJSON); err != nil { + return nil, fmt.Errorf("failed to unmarshal server JSON: %w", err) + } + + // Return the updated ServerResponse + serverResponse := &apiv0.ServerResponse{ + Server: serverJSON, + Meta: apiv0.ResponseMeta{ + Official: &apiv0.RegistryExtensions{ + Status: model.Status(currentStatus), + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + IsLatest: isLatest, + }, + }, + } + + return serverResponse, nil } // InTransaction executes a function within a database transaction @@ -479,6 +606,112 @@ func hashServerName(name string) int64 { return int64(hash & 0x7FFFFFFFFFFFFFFF) } +// GetCurrentLatestVersion retrieves the current latest version of a server by server name +func (db *PostgreSQL) GetCurrentLatestVersion(ctx context.Context, tx pgx.Tx, serverName string) (*apiv0.ServerResponse, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + executor := db.getExecutor(tx) + + query := ` + SELECT server_name, version, status, value, published_at, updated_at, is_latest + FROM servers + WHERE server_name = $1 AND is_latest = true + ` + + row := executor.QueryRow(ctx, query, serverName) + + var name, version, status string + var publishedAt, updatedAt time.Time + var isLatest bool + var jsonValue []byte + + err := row.Scan(&name, &version, &status, &jsonValue, &publishedAt, &updatedAt, &isLatest) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("failed to scan server row: %w", err) + } + + // Parse the JSON value to get the server details + var serverJSON apiv0.ServerJSON + if err := json.Unmarshal(jsonValue, &serverJSON); err != nil { + return nil, fmt.Errorf("failed to unmarshal server JSON: %w", err) + } + + // Build ServerResponse with separated metadata + serverResponse := &apiv0.ServerResponse{ + Server: serverJSON, + Meta: apiv0.ResponseMeta{ + Official: &apiv0.RegistryExtensions{ + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + IsLatest: isLatest, + }, + }, + } + + return serverResponse, nil +} + +// CountServerVersions counts the number of versions for a server +func (db *PostgreSQL) CountServerVersions(ctx context.Context, tx pgx.Tx, serverName string) (int, error) { + if ctx.Err() != nil { + return 0, ctx.Err() + } + + executor := db.getExecutor(tx) + + query := `SELECT COUNT(*) FROM servers WHERE server_name = $1` + + var count int + err := executor.QueryRow(ctx, query, serverName).Scan(&count) + if err != nil { + return 0, fmt.Errorf("failed to count server versions: %w", err) + } + + return count, nil +} + +// CheckVersionExists checks if a specific version exists for a server +func (db *PostgreSQL) CheckVersionExists(ctx context.Context, tx pgx.Tx, serverName, version string) (bool, error) { + if ctx.Err() != nil { + return false, ctx.Err() + } + + executor := db.getExecutor(tx) + + query := `SELECT EXISTS(SELECT 1 FROM servers WHERE server_name = $1 AND version = $2)` + + var exists bool + err := executor.QueryRow(ctx, query, serverName, version).Scan(&exists) + if err != nil { + return false, fmt.Errorf("failed to check version existence: %w", err) + } + + return exists, nil +} + +// UnmarkAsLatest marks the current latest version of a server as no longer latest +func (db *PostgreSQL) UnmarkAsLatest(ctx context.Context, tx pgx.Tx, serverName string) error { + if ctx.Err() != nil { + return ctx.Err() + } + + executor := db.getExecutor(tx) + + query := `UPDATE servers SET is_latest = false WHERE server_name = $1 AND is_latest = true` + + _, err := executor.Exec(ctx, query, serverName) + if err != nil { + return fmt.Errorf("failed to unmark latest version: %w", err) + } + + return nil +} + // Close closes the database connection func (db *PostgreSQL) Close() error { db.pool.Close() diff --git a/internal/database/postgres_test.go b/internal/database/postgres_test.go new file mode 100644 index 0000000..7606b79 --- /dev/null +++ b/internal/database/postgres_test.go @@ -0,0 +1,969 @@ +package database_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/modelcontextprotocol/registry/internal/database" + apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPostgreSQL_CreateServer(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + tests := []struct { + name string + serverJSON *apiv0.ServerJSON + officialMeta *apiv0.RegistryExtensions + expectError bool + errorType error + }{ + { + name: "successful server creation", + serverJSON: &apiv0.ServerJSON{ + Name: "com.example/test-server", + Description: "A test server", + Version: "1.0.0", + Remotes: []model.Transport{ + {Type: "http", URL: "https://api.example.com/mcp"}, + }, + }, + officialMeta: &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + }, + expectError: false, + }, + { + name: "duplicate server version should fail", + serverJSON: &apiv0.ServerJSON{ + Name: "com.example/duplicate-server", + Description: "A duplicate test server", + Version: "1.0.0", + }, + officialMeta: &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + }, + expectError: true, + // Note: Expecting generic database error for constraint violation + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create the first server to test duplicates + if tt.name == "duplicate server version should fail" { + _, err := db.CreateServer(ctx, nil, tt.serverJSON, tt.officialMeta) + require.NoError(t, err, "First creation should succeed") + } + + result, err := db.CreateServer(ctx, nil, tt.serverJSON, tt.officialMeta) + + if tt.expectError { + assert.Error(t, err) + if tt.errorType != nil { + assert.ErrorIs(t, err, tt.errorType) + } + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.serverJSON.Name, result.Server.Name) + assert.Equal(t, tt.serverJSON.Version, result.Server.Version) + assert.Equal(t, tt.serverJSON.Description, result.Server.Description) + assert.NotNil(t, result.Meta.Official) + assert.Equal(t, tt.officialMeta.Status, result.Meta.Official.Status) + assert.Equal(t, tt.officialMeta.IsLatest, result.Meta.Official.IsLatest) + } + }) + } +} + +func TestPostgreSQL_GetServerByName(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + // Setup test data + serverJSON := &apiv0.ServerJSON{ + Name: "com.example/get-test-server", + Description: "A server for get testing", + Version: "1.0.0", + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + } + + // Create the server + _, err := db.CreateServer(ctx, nil, serverJSON, officialMeta) + require.NoError(t, err) + + tests := []struct { + name string + serverName string + expectError bool + errorType error + }{ + { + name: "get existing server", + serverName: "com.example/get-test-server", + }, + { + name: "get non-existent server", + serverName: "com.example/non-existent", + expectError: true, + errorType: database.ErrNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := db.GetServerByName(ctx, nil, tt.serverName) + + if tt.expectError { + assert.Error(t, err) + if tt.errorType != nil { + assert.ErrorIs(t, err, tt.errorType) + } + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.serverName, result.Server.Name) + assert.NotNil(t, result.Meta.Official) + } + }) + } +} + +func TestPostgreSQL_GetServerByNameAndVersion(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + // Setup test data with multiple versions + serverName := "com.example/version-test-server" + versions := []string{"1.0.0", "1.1.0", "2.0.0"} + + for i, version := range versions { + serverJSON := &apiv0.ServerJSON{ + Name: serverName, + Description: "A server for version testing", + Version: version, + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: i == len(versions)-1, // Only last version is latest + } + + _, err := db.CreateServer(ctx, nil, serverJSON, officialMeta) + require.NoError(t, err) + } + + tests := []struct { + name string + serverName string + version string + expectError bool + errorType error + }{ + { + name: "get existing server version", + serverName: serverName, + version: "1.1.0", + }, + { + name: "get non-existent version", + serverName: serverName, + version: "3.0.0", + expectError: true, + errorType: database.ErrNotFound, + }, + { + name: "get non-existent server", + serverName: "com.example/non-existent", + version: "1.0.0", + expectError: true, + errorType: database.ErrNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := db.GetServerByNameAndVersion(ctx, nil, tt.serverName, tt.version) + + if tt.expectError { + assert.Error(t, err) + if tt.errorType != nil { + assert.ErrorIs(t, err, tt.errorType) + } + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.serverName, result.Server.Name) + assert.Equal(t, tt.version, result.Server.Version) + assert.NotNil(t, result.Meta.Official) + } + }) + } +} + +func TestPostgreSQL_ListServers(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + // Setup test data + testServers := []struct { + name string + version string + status model.Status + remoteURL string + isLatest bool + publishedAt time.Time + }{ + { + name: "com.example/server-a", + version: "1.0.0", + status: model.StatusActive, + remoteURL: "https://api-a.example.com/mcp", + isLatest: true, + publishedAt: time.Now().Add(-2 * time.Hour), + }, + { + name: "com.example/server-b", + version: "2.0.0", + status: model.StatusActive, + remoteURL: "https://api-b.example.com/mcp", + isLatest: true, + publishedAt: time.Now().Add(-1 * time.Hour), + }, + { + name: "com.example/server-c", + version: "1.0.0", + status: model.StatusDeprecated, + remoteURL: "https://api-c.example.com/mcp", + isLatest: true, + publishedAt: time.Now().Add(-30 * time.Minute), + }, + } + + // Create test servers + for _, server := range testServers { + serverJSON := &apiv0.ServerJSON{ + Name: server.name, + Description: "Test server for listing", + Version: server.version, + Remotes: []model.Transport{ + {Type: "http", URL: server.remoteURL}, + }, + } + officialMeta := &apiv0.RegistryExtensions{ + Status: server.status, + PublishedAt: server.publishedAt, + UpdatedAt: server.publishedAt, + IsLatest: server.isLatest, + } + + _, err := db.CreateServer(ctx, nil, serverJSON, officialMeta) + require.NoError(t, err) + } + + tests := []struct { + name string + filter *database.ServerFilter + cursor string + limit int + expectedCount int + expectedNames []string + expectError bool + }{ + { + name: "list all servers", + filter: nil, + limit: 10, + expectedCount: 3, + expectedNames: []string{"com.example/server-a", "com.example/server-b", "com.example/server-c"}, + }, + { + name: "filter by name", + filter: &database.ServerFilter{ + Name: stringPtr("com.example/server-a"), + }, + limit: 10, + expectedCount: 1, + expectedNames: []string{"com.example/server-a"}, + }, + { + name: "filter by remote URL", + filter: &database.ServerFilter{ + RemoteURL: stringPtr("https://api-b.example.com/mcp"), + }, + limit: 10, + expectedCount: 1, + expectedNames: []string{"com.example/server-b"}, + }, + { + name: "filter by substring name", + filter: &database.ServerFilter{ + SubstringName: stringPtr("server-"), + }, + limit: 10, + expectedCount: 3, + }, + { + name: "filter by version", + filter: &database.ServerFilter{ + Version: stringPtr("1.0.0"), + }, + limit: 10, + expectedCount: 2, + }, + { + name: "filter by isLatest", + filter: &database.ServerFilter{ + IsLatest: boolPtr(true), + }, + limit: 10, + expectedCount: 3, + }, + { + name: "filter by updatedSince", + filter: &database.ServerFilter{ + UpdatedSince: timePtr(time.Now().Add(-45 * time.Minute)), + }, + limit: 10, + expectedCount: 1, // Only server-c was updated in the last 45 minutes + }, + { + name: "test pagination with limit", + filter: nil, + limit: 2, + expectedCount: 2, + }, + { + name: "test cursor pagination", + filter: nil, + cursor: "com.example/server-a", + limit: 10, + // Should return servers after 'server-a' alphabetically + expectedCount: 2, + expectedNames: []string{"com.example/server-b", "com.example/server-c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results, nextCursor, err := db.ListServers(ctx, nil, tt.filter, tt.cursor, tt.limit) + + if tt.expectError { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Len(t, results, tt.expectedCount) + + if len(tt.expectedNames) > 0 { + actualNames := make([]string, len(results)) + for i, result := range results { + actualNames[i] = result.Server.Name + } + assert.Subset(t, tt.expectedNames, actualNames) + } + + // Test cursor behavior + if tt.limit < len(testServers) && len(results) == tt.limit { + assert.NotEmpty(t, nextCursor, "Should return next cursor when results are limited") + } + }) + } +} + +func TestPostgreSQL_UpdateServer(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + // Setup test data + serverName := "com.example/update-test-server" + version := "1.0.0" + serverJSON := &apiv0.ServerJSON{ + Name: serverName, + Description: "Original description", + Version: version, + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + } + + _, err := db.CreateServer(ctx, nil, serverJSON, officialMeta) + require.NoError(t, err) + + tests := []struct { + name string + serverName string + version string + updatedServer *apiv0.ServerJSON + expectError bool + errorType error + }{ + { + name: "successful server update", + serverName: serverName, + version: version, + updatedServer: &apiv0.ServerJSON{ + Name: serverName, + Description: "Updated description", + Version: version, + Remotes: []model.Transport{ + {Type: "http", URL: "https://updated.example.com/mcp"}, + }, + }, + }, + { + name: "update non-existent server", + serverName: "com.example/non-existent", + version: "1.0.0", + updatedServer: &apiv0.ServerJSON{ + Name: "com.example/non-existent", + Description: "Should fail", + Version: "1.0.0", + }, + expectError: true, + errorType: database.ErrNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := db.UpdateServer(ctx, nil, tt.serverName, tt.version, tt.updatedServer) + + if tt.expectError { + assert.Error(t, err) + if tt.errorType != nil { + assert.ErrorIs(t, err, tt.errorType) + } + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.updatedServer.Description, result.Server.Description) + assert.NotNil(t, result.Meta.Official) + assert.NotZero(t, result.Meta.Official.UpdatedAt) + } + }) + } +} + +func TestPostgreSQL_SetServerStatus(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + // Setup test data + serverName := "com.example/status-test-server" + version := "1.0.0" + serverJSON := &apiv0.ServerJSON{ + Name: serverName, + Description: "A server for status testing", + Version: version, + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + } + + _, err := db.CreateServer(ctx, nil, serverJSON, officialMeta) + require.NoError(t, err) + + tests := []struct { + name string + serverName string + version string + newStatus string + expectError bool + errorType error + }{ + { + name: "active to deprecated", + serverName: serverName, + version: version, + newStatus: string(model.StatusDeprecated), + }, + { + name: "invalid status", + serverName: serverName, + version: version, + newStatus: "invalid_status", + expectError: true, + }, + { + name: "non-existent server", + serverName: "com.example/non-existent", + version: "1.0.1", + newStatus: string(model.StatusDeprecated), + expectError: true, + errorType: database.ErrNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := db.SetServerStatus(ctx, nil, tt.serverName, tt.version, tt.newStatus) + + if tt.expectError { + assert.Error(t, err) + if tt.errorType != nil { + assert.ErrorIs(t, err, tt.errorType) + } + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, model.Status(tt.newStatus), result.Meta.Official.Status) + assert.NotZero(t, result.Meta.Official.UpdatedAt) + } + }) + } +} + +func TestPostgreSQL_TransactionHandling(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + t.Run("successful transaction", func(t *testing.T) { + err := db.InTransaction(ctx, func(ctx context.Context, tx pgx.Tx) error { + serverJSON := &apiv0.ServerJSON{ + Name: "com.example/transaction-success", + Description: "Transaction test server", + Version: "1.0.0", + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + } + + _, err := db.CreateServer(ctx, tx, serverJSON, officialMeta) + return err + }) + + assert.NoError(t, err) + + // Verify server was created + result, err := db.GetServerByName(ctx, nil, "com.example/transaction-success") + assert.NoError(t, err) + assert.NotNil(t, result) + }) + + t.Run("failed transaction rollback", func(t *testing.T) { + err := db.InTransaction(ctx, func(ctx context.Context, tx pgx.Tx) error { + serverJSON := &apiv0.ServerJSON{ + Name: "com.example/transaction-rollback", + Description: "Transaction rollback test server", + Version: "1.0.0", + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + } + + _, err := db.CreateServer(ctx, tx, serverJSON, officialMeta) + if err != nil { + return err + } + + // Force an error to trigger rollback + return assert.AnError + }) + + assert.Error(t, err) + assert.Equal(t, assert.AnError, err) + + // Verify server was NOT created due to rollback + result, err := db.GetServerByName(ctx, nil, "com.example/transaction-rollback") + assert.Error(t, err) + assert.ErrorIs(t, err, database.ErrNotFound) + assert.Nil(t, result) + }) +} + +func TestPostgreSQL_ConcurrencyAndLocking(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + serverName := "com.example/concurrent-server" + + // Test advisory locking prevents concurrent publishes + t.Run("advisory locking prevents race conditions", func(t *testing.T) { + published := make(chan bool, 2) + errors := make(chan error, 2) + + // Launch two concurrent publish operations + for i := 0; i < 2; i++ { + go func(version string) { + err := db.InTransaction(ctx, func(ctx context.Context, tx pgx.Tx) error { + // Acquire lock + if err := db.AcquirePublishLock(ctx, tx, serverName); err != nil { + return err + } + + // Simulate some processing time + time.Sleep(100 * time.Millisecond) + + serverJSON := &apiv0.ServerJSON{ + Name: serverName, + Description: "Concurrent test server", + Version: version, + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + } + + _, err := db.CreateServer(ctx, tx, serverJSON, officialMeta) + if err != nil { + return err + } + + published <- true + return nil + }) + errors <- err + }(time.Now().Format("20060102150405.000000")) + } + + // Wait for both goroutines to complete + err1 := <-errors + err2 := <-errors + + // One should succeed, one should wait (or fail if timeout) + successCount := 0 + if err1 == nil { + successCount++ + } + if err2 == nil { + successCount++ + } + + // At least one should succeed (both can succeed if advisory lock works properly) + assert.GreaterOrEqual(t, successCount, 1, "At least one concurrent operation should succeed") + + close(published) + close(errors) + }) +} + +func TestPostgreSQL_HelperMethods(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + serverName := "com.example/helper-test-server" + + // Setup test data with multiple versions + versions := []string{"1.0.0", "1.1.0", "2.0.0"} + for _, version := range versions { + serverJSON := &apiv0.ServerJSON{ + Name: serverName, + Description: "Helper methods test server", + Version: version, + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: version == "2.0.0", + } + + _, err := db.CreateServer(ctx, nil, serverJSON, officialMeta) + require.NoError(t, err) + } + + t.Run("CountServerVersions", func(t *testing.T) { + count, err := db.CountServerVersions(ctx, nil, serverName) + assert.NoError(t, err) + assert.Equal(t, 3, count) + + // Test non-existent server + count, err = db.CountServerVersions(ctx, nil, "com.example/non-existent") + assert.NoError(t, err) + assert.Equal(t, 0, count) + }) + + t.Run("CheckVersionExists", func(t *testing.T) { + exists, err := db.CheckVersionExists(ctx, nil, serverName, "1.1.0") + assert.NoError(t, err) + assert.True(t, exists) + + exists, err = db.CheckVersionExists(ctx, nil, serverName, "3.0.0") + assert.NoError(t, err) + assert.False(t, exists) + }) + + t.Run("GetCurrentLatestVersion", func(t *testing.T) { + latest, err := db.GetCurrentLatestVersion(ctx, nil, serverName) + assert.NoError(t, err) + assert.NotNil(t, latest) + assert.Equal(t, "2.0.0", latest.Server.Version) + assert.True(t, latest.Meta.Official.IsLatest) + }) + + t.Run("GetAllVersionsByServerName", func(t *testing.T) { + allVersions, err := db.GetAllVersionsByServerName(ctx, nil, serverName) + assert.NoError(t, err) + assert.Len(t, allVersions, 3) + + // Check versions are present + versionSet := make(map[string]bool) + for _, server := range allVersions { + versionSet[server.Server.Version] = true + } + for _, expectedVersion := range versions { + assert.True(t, versionSet[expectedVersion], "Version %s should be present", expectedVersion) + } + }) + + t.Run("UnmarkAsLatest", func(t *testing.T) { + err := db.UnmarkAsLatest(ctx, nil, serverName) + assert.NoError(t, err) + + // Verify no version is marked as latest + latest, err := db.GetCurrentLatestVersion(ctx, nil, serverName) + assert.Error(t, err) + assert.ErrorIs(t, err, database.ErrNotFound) + assert.Nil(t, latest) + }) +} + +func TestPostgreSQL_EdgeCases(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + t.Run("input validation", func(t *testing.T) { + // Test nil inputs + _, err := db.CreateServer(ctx, nil, nil, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "serverJSON and officialMeta are required") + + // Test empty required fields + _, err = db.CreateServer(ctx, nil, &apiv0.ServerJSON{}, &apiv0.RegistryExtensions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "server name and version are required") + }) + + t.Run("database constraints", func(t *testing.T) { + // Test server name format constraint (should be caught by database constraint) + invalidServer := &apiv0.ServerJSON{ + Name: "invalid-name-format", // Missing namespace/name format + Description: "Invalid server", + Version: "1.0.0", + } + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + } + + _, err := db.CreateServer(ctx, nil, invalidServer, officialMeta) + assert.Error(t, err, "Should fail due to server name format constraint") + }) + + t.Run("pagination edge cases", func(t *testing.T) { + // Test pagination with no results + results, cursor, err := db.ListServers(ctx, nil, &database.ServerFilter{ + Name: stringPtr("com.example/non-existent-server"), + }, "", 10) + assert.NoError(t, err) + assert.Empty(t, results) + assert.Empty(t, cursor) + + // Test pagination with limit 0 (should use default) + _, _, err = db.ListServers(ctx, nil, nil, "", 0) + assert.NoError(t, err) + // Should still work with default limit + }) + + t.Run("complex filtering", func(t *testing.T) { + // Setup test data + serverName := "com.example/complex-filter-server" + testTime := time.Now().Add(-1 * time.Hour) + + _, err := db.CreateServer(ctx, nil, &apiv0.ServerJSON{ + Name: serverName, + Description: "Complex filter test server", + Version: "1.0.0", + Remotes: []model.Transport{ + {Type: "streamable-http", URL: "https://complex.example.com/mcp"}, + }, + }, &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: testTime, + UpdatedAt: testTime, + IsLatest: true, + }) + require.NoError(t, err) + + // Test multiple filters combined + filter := &database.ServerFilter{ + SubstringName: stringPtr("complex"), + UpdatedSince: timePtr(testTime.Add(-30 * time.Minute)), + IsLatest: boolPtr(true), + Version: stringPtr("1.0.0"), + } + + results, _, err := db.ListServers(ctx, nil, filter, "", 10) + assert.NoError(t, err) + assert.Len(t, results, 1) + assert.Equal(t, serverName, results[0].Server.Name) + }) + + t.Run("status transitions", func(t *testing.T) { + serverName := "com.example/status-transition-server" + version := "1.0.2" + + // Create server + _, err := db.CreateServer(ctx, nil, &apiv0.ServerJSON{ + Name: serverName, + Description: "Status transition test", + Version: version, + }, &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + }) + require.NoError(t, err) + + // Test all valid status transitions + statuses := []string{ + string(model.StatusDeprecated), + string(model.StatusDeleted), + string(model.StatusActive), // Can transition back + } + + for _, status := range statuses { + result, err := db.SetServerStatus(ctx, nil, serverName, version, status) + assert.NoError(t, err, "Should allow transition to %s", status) + assert.Equal(t, model.Status(status), result.Meta.Official.Status) + } + }) +} + +func TestPostgreSQL_PerformanceScenarios(t *testing.T) { + db := database.NewTestDB(t) + ctx := context.Background() + + t.Run("many versions management", func(t *testing.T) { + serverName := "com.example/many-versions-server" + + // Create many versions (but stay under the limit) + versionCount := 50 + for i := 0; i < versionCount; i++ { + _, err := db.CreateServer(ctx, nil, &apiv0.ServerJSON{ + Name: serverName, + Description: fmt.Sprintf("Version %d", i), + Version: fmt.Sprintf("1.0.%d", i), + }, &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: i == versionCount-1, // Only last one is latest + }) + require.NoError(t, err) + } + + // Test counting versions + count, err := db.CountServerVersions(ctx, nil, serverName) + assert.NoError(t, err) + assert.Equal(t, versionCount, count) + + // Test getting all versions + allVersions, err := db.GetAllVersionsByServerName(ctx, nil, serverName) + assert.NoError(t, err) + assert.Len(t, allVersions, versionCount) + + // Test only one is marked as latest + latestCount := 0 + for _, version := range allVersions { + if version.Meta.Official.IsLatest { + latestCount++ + } + } + assert.Equal(t, 1, latestCount) + }) + + t.Run("large result pagination", func(t *testing.T) { + // Create multiple servers for pagination testing + serverCount := 25 + for i := 0; i < serverCount; i++ { + _, err := db.CreateServer(ctx, nil, &apiv0.ServerJSON{ + Name: fmt.Sprintf("com.example/pagination-server-%02d", i), + Description: "Pagination test server", + Version: "1.0.0", + }, &apiv0.RegistryExtensions{ + Status: model.StatusActive, + PublishedAt: time.Now(), + UpdatedAt: time.Now(), + IsLatest: true, + }) + require.NoError(t, err) + } + + // Test paginated retrieval + allResults := []*apiv0.ServerResponse{} + cursor := "" + pageSize := 10 + + for { + results, nextCursor, err := db.ListServers(ctx, nil, nil, cursor, pageSize) + assert.NoError(t, err) + allResults = append(allResults, results...) + + if nextCursor == "" || len(results) < pageSize { + break + } + cursor = nextCursor + } + + // Should have retrieved all servers including the ones we just created + assert.GreaterOrEqual(t, len(allResults), serverCount) + }) +} + +// Helper functions for creating pointers to basic types +func stringPtr(s string) *string { + return &s +} + +func boolPtr(b bool) *bool { + return &b +} + +func timePtr(t time.Time) *time.Time { + return &t +} \ No newline at end of file diff --git a/internal/importer/importer.go b/internal/importer/importer.go index fd63db5..3b6509d 100644 --- a/internal/importer/importer.go +++ b/internal/importer/importer.go @@ -10,24 +10,24 @@ import ( "os" "strings" - "github.com/modelcontextprotocol/registry/internal/database" + "github.com/modelcontextprotocol/registry/internal/service" "github.com/modelcontextprotocol/registry/internal/validators" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" ) // Service handles importing seed data into the registry type Service struct { - db database.Database + registry service.RegistryService } // NewService creates a new importer service -func NewService(db database.Database) *Service { - return &Service{db: db} +func NewService(registry service.RegistryService) *Service { + return &Service{registry: registry} } // ImportFromPath imports seed data from various sources: -// 1. Local file paths (*.json files) - expects extension wrapper format -// 2. Direct HTTP URLs to seed.json files - expects extension wrapper format +// 1. Local file paths (*.json files) - expects ServerJSON array format +// 2. Direct HTTP URLs to seed.json files - expects ServerJSON array format // 3. Registry root URLs (automatically appends /v0/servers and paginates) func (s *Service) ImportFromPath(ctx context.Context, path string) error { servers, err := readSeedFile(ctx, path) @@ -35,14 +35,29 @@ func (s *Service) ImportFromPath(ctx context.Context, path string) error { return fmt.Errorf("failed to read seed data: %w", err) } - // Import each server using CreateServer + // Import each server using registry service CreateServer + var successfullyCreated []string + var failedCreations []string + for _, server := range servers { - _, err := s.db.CreateServer(ctx, nil, server) + _, err := s.registry.CreateServer(ctx, server) if err != nil { - return fmt.Errorf("failed to import server %s: %w", server.Name, err) + failedCreations = append(failedCreations, fmt.Sprintf("%s: %v", server.Name, err)) + log.Printf("Failed to create server %s: %v", server.Name, err) + } else { + successfullyCreated = append(successfullyCreated, server.Name) } } + // Report import results after actual creation attempts + if len(failedCreations) > 0 { + log.Printf("Import completed with errors: %d servers created successfully, %d servers failed", + len(successfullyCreated), len(failedCreations)) + log.Printf("Failed servers: %v", failedCreations) + return fmt.Errorf("failed to import %d servers", len(failedCreations)) + } + + log.Printf("Import completed successfully: all %d servers created", len(successfullyCreated)) return nil } @@ -69,10 +84,10 @@ func readSeedFile(ctx context.Context, path string) ([]*apiv0.ServerJSON, error) return nil, fmt.Errorf("failed to read seed data from %s: %w", path, err) } - // Parse extension wrapper format (only supported format) + // Parse ServerJSON array format var serverResponses []apiv0.ServerJSON if err := json.Unmarshal(data, &serverResponses); err != nil { - return nil, fmt.Errorf("failed to parse seed data as extension wrapper format: %w", err) + return nil, fmt.Errorf("failed to parse seed data as ServerJSON array format: %w", err) } if len(serverResponses) == 0 { @@ -93,20 +108,19 @@ func readSeedFile(ctx context.Context, path string) ([]*apiv0.ServerJSON, error) continue } - // Convert valid ServerJSON to ServerRecord - record := convertServerResponseToRecord(response) - validRecords = append(validRecords, record) + // Add valid ServerJSON to records + validRecords = append(validRecords, &response) } // Print summary of validation results if len(invalidServers) > 0 { - log.Printf("Import summary: %d valid servers imported, %d invalid servers skipped", len(validRecords), len(invalidServers)) + log.Printf("Validation summary: %d servers passed validation, %d invalid servers skipped", len(validRecords), len(invalidServers)) log.Printf("Invalid servers: %v", invalidServers) for _, failure := range validationFailures { log.Printf(" - %s", failure) } } else { - log.Printf("Import summary: All %d servers imported successfully", len(validRecords)) + log.Printf("Validation summary: All %d servers passed validation", len(validRecords)) } return validRecords, nil @@ -151,9 +165,9 @@ func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*apiv0.ServerJ } var response struct { - Servers []apiv0.ServerJSON `json:"servers"` + Servers []apiv0.ServerResponse `json:"servers"` Metadata *struct { - NextCursor string `json:"next_cursor,omitempty"` + NextCursor string `json:"nextCursor,omitempty"` } `json:"metadata,omitempty"` } @@ -161,10 +175,9 @@ func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*apiv0.ServerJ return nil, fmt.Errorf("failed to parse registry API response: %w", err) } - // Convert and add servers + // Extract ServerJSON from each ServerResponse for _, serverResponse := range response.Servers { - record := convertServerResponseToRecord(serverResponse) - allRecords = append(allRecords, record) + allRecords = append(allRecords, &serverResponse.Server) } // Check if there's a next page @@ -177,8 +190,3 @@ func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*apiv0.ServerJ return allRecords, nil } -func convertServerResponseToRecord(response apiv0.ServerJSON) *apiv0.ServerJSON { - // The response is already in the correct flattened format - // Just return a pointer to it - return &response -} diff --git a/internal/importer/importer_test.go b/internal/importer/importer_test.go index 04b4d9e..f7e8a2b 100644 --- a/internal/importer/importer_test.go +++ b/internal/importer/importer_test.go @@ -7,10 +7,11 @@ import ( "net/http/httptest" "os" "testing" - "time" + "github.com/modelcontextprotocol/registry/internal/config" "github.com/modelcontextprotocol/registry/internal/database" "github.com/modelcontextprotocol/registry/internal/importer" + "github.com/modelcontextprotocol/registry/internal/service" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" "github.com/stretchr/testify/assert" @@ -20,7 +21,7 @@ import ( func TestImportService_LocalFile(t *testing.T) { // Create a temporary seed file tempFile := "/tmp/test_import_seed.json" - seedData := []apiv0.ServerJSON{ + seedData := []*apiv0.ServerJSON{ { Name: "io.github.test/test-server-1", Description: "Test server 1", @@ -30,15 +31,6 @@ func TestImportService_LocalFile(t *testing.T) { ID: "123", }, Version: "1.0.0", - Meta: &apiv0.ServerMeta{ - Official: &apiv0.RegistryExtensions{ - ServerID: "server-id-1", - VersionID: "test-id-1", - PublishedAt: time.Now(), - UpdatedAt: time.Now(), - IsLatest: true, - }, - }, }, } @@ -49,149 +41,201 @@ func TestImportService_LocalFile(t *testing.T) { require.NoError(t, err) defer os.Remove(tempFile) + // Create registry service testDB := database.NewTestDB(t) + registryService := service.NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) // Create importer service and test import - service := importer.NewService(testDB) - err = service.ImportFromPath(context.Background(), tempFile) + importerService := importer.NewService(registryService) + err = importerService.ImportFromPath(context.Background(), tempFile) require.NoError(t, err) - // Verify the server was imported - servers, _, err := testDB.List(context.Background(), nil, nil, "", 10) + // Verify the server was imported using registry service + servers, _, err := registryService.ListServers(context.Background(), nil, "", 10) require.NoError(t, err) assert.Len(t, servers, 1) - assert.Equal(t, "io.github.test/test-server-1", servers[0].Name) + assert.Equal(t, "io.github.test/test-server-1", servers[0].Server.Name) + assert.Equal(t, "1.0.0", servers[0].Server.Version) + assert.Equal(t, "Test server 1", servers[0].Server.Description) + assert.NotNil(t, servers[0].Meta.Official) + assert.Equal(t, model.StatusActive, servers[0].Meta.Official.Status) } func TestImportService_HTTPFile(t *testing.T) { // Create a test HTTP server - seedData := []apiv0.ServerJSON{ + seedData := []*apiv0.ServerJSON{ { Name: "io.github.test/http-test-server", Description: "HTTP test server", Repository: model.Repository{ - URL: "https://github.com/test/repo1", - Source: "github", - ID: "123", - }, - Version: "2.0.0", - Meta: &apiv0.ServerMeta{ - Official: &apiv0.RegistryExtensions{ - ServerID: "server-id-test", - VersionID: "test-id-2", - PublishedAt: time.Now(), - UpdatedAt: time.Now(), - IsLatest: true, - }, - }, - }, - } - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(seedData) - })) - defer server.Close() - - testDB := database.NewTestDB(t) - - // Create importer service and test import - service := importer.NewService(testDB) - err := service.ImportFromPath(context.Background(), server.URL+"/seed.json") - require.NoError(t, err) - - // Verify the server was imported - servers, _, err := testDB.List(context.Background(), nil, nil, "", 10) - require.NoError(t, err) - assert.Len(t, servers, 1) - assert.Equal(t, "io.github.test/http-test-server", servers[0].Name) -} - -func TestImportService_RegistryAPI(t *testing.T) { - // Create test data - testServers := []apiv0.ServerJSON{ - { - Name: "io.github.test/api-server-1", - Description: "API server 1", - Repository: model.Repository{ - URL: "https://github.com/test/repo1", - Source: "github", - ID: "123", - }, - Version: "1.0.0", - Meta: &apiv0.ServerMeta{ - Official: &apiv0.RegistryExtensions{ - ServerID: "server-id-test-1", - VersionID: "api-test-id-1", - PublishedAt: time.Now(), - UpdatedAt: time.Now(), - IsLatest: true, - }, - }, - }, - { - Name: "io.github.test/api-server-2", - Description: "API server 2", - Repository: model.Repository{ - URL: "https://github.com/test/repo2", + URL: "https://github.com/test/http-repo", Source: "github", ID: "456", }, Version: "2.0.0", - Meta: &apiv0.ServerMeta{ - Official: &apiv0.RegistryExtensions{ - ServerID: "server-id-test-2", - VersionID: "api-test-id-2", - PublishedAt: time.Now(), - UpdatedAt: time.Now(), - IsLatest: true, - }, - }, }, } - // Create test server that responds with paginated results - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - cursor := r.URL.Query().Get("cursor") + jsonData, err := json.Marshal(seedData) + require.NoError(t, err) - var response struct { - Servers []apiv0.ServerJSON `json:"servers"` - Metadata *struct { - NextCursor string `json:"next_cursor,omitempty"` - } `json:"metadata,omitempty"` + // Create test HTTP server + httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + })) + defer httpServer.Close() + + // Create registry service + testDB := database.NewTestDB(t) + registryService := service.NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) + + // Create importer service and test import + importerService := importer.NewService(registryService) + err = importerService.ImportFromPath(context.Background(), httpServer.URL+"/seed.json") + require.NoError(t, err) + + // Verify the server was imported + servers, _, err := registryService.ListServers(context.Background(), nil, "", 10) + require.NoError(t, err) + assert.Len(t, servers, 1) + assert.Equal(t, "io.github.test/http-test-server", servers[0].Server.Name) + assert.Equal(t, "2.0.0", servers[0].Server.Version) + assert.Equal(t, "HTTP test server", servers[0].Server.Description) + assert.NotNil(t, servers[0].Meta.Official) +} + +func TestImportService_RegistryPagination(t *testing.T) { + ctx := context.Background() + + // Create registry service with test data + testDB := database.NewTestDB(t) + registryService := service.NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) + + // Setup source registry with test data + sourceServers := []*apiv0.ServerJSON{ + { + Name: "com.source/server-1", + Description: "Source server 1", + Version: "1.0.0", + }, + { + Name: "com.source/server-2", + Description: "Source server 2", + Version: "1.0.0", + }, + } + + for _, server := range sourceServers { + _, err := registryService.CreateServer(ctx, server) + require.NoError(t, err) + } + + // Create test HTTP server that serves the registry API + httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + servers, _, _ := registryService.ListServers(ctx, nil, "", 10) + + // Convert to response format + serverValues := make([]apiv0.ServerResponse, len(servers)) + for i, server := range servers { + serverValues[i] = *server } - switch cursor { - case "": - // First page - response.Servers = []apiv0.ServerJSON{testServers[0]} - response.Metadata = &struct { - NextCursor string `json:"next_cursor,omitempty"` - }{NextCursor: "page2"} - case "page2": - // Second page - response.Servers = []apiv0.ServerJSON{testServers[1]} - // No next cursor means end of results + response := apiv0.ServerListResponse{ + Servers: serverValues, + Metadata: apiv0.Metadata{ + Count: len(servers), + }, } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response) })) - defer server.Close() + defer httpServer.Close() - testDB := database.NewTestDB(t) + // Create target registry for import + targetDB := database.NewTestDB(t) + targetRegistryService := service.NewRegistryService(targetDB, &config.Config{EnableRegistryValidation: false}) - // Create importer service and test import - service := importer.NewService(testDB) - err := service.ImportFromPath(context.Background(), server.URL+"/v0/servers") + // Create importer service and test registry import + importerService := importer.NewService(targetRegistryService) + err := importerService.ImportFromPath(context.Background(), httpServer.URL+"/v0/servers") require.NoError(t, err) - // Verify both servers were imported - servers, _, err := testDB.List(context.Background(), nil, nil, "", 10) + // Verify servers were imported + importedServers, _, err := targetRegistryService.ListServers(context.Background(), nil, "", 10) require.NoError(t, err) - assert.Len(t, servers, 2) + assert.Len(t, importedServers, 2) - names := []string{servers[0].Name, servers[1].Name} - assert.Contains(t, names, "io.github.test/api-server-1") - assert.Contains(t, names, "io.github.test/api-server-2") + // Verify server details + serverNames := make([]string, len(importedServers)) + for i, server := range importedServers { + serverNames[i] = server.Server.Name + } + assert.Contains(t, serverNames, "com.source/server-1") + assert.Contains(t, serverNames, "com.source/server-2") +} + +func TestImportService_ErrorHandling(t *testing.T) { + // Create registry service + testDB := database.NewTestDB(t) + registryService := service.NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) + importerService := importer.NewService(registryService) + + tests := []struct { + name string + path string + expectError bool + errorMsg string + }{ + { + name: "non-existent local file", + path: "/tmp/non-existent-file.json", + expectError: true, + errorMsg: "failed to read seed data", + }, + { + name: "invalid JSON file", + path: "/tmp/invalid.json", + expectError: true, + errorMsg: "failed to read seed data", + }, + { + name: "non-existent HTTP URL", + path: "http://non-existent-domain-12345.com/seed.json", + expectError: true, + errorMsg: "failed to read seed data", + }, + } + + // Create invalid JSON file for testing + invalidJSON := []byte("{invalid json}") + tempFile, err := os.CreateTemp("", "invalid-*.json") + require.NoError(t, err) + defer os.Remove(tempFile.Name()) + err = os.WriteFile(tempFile.Name(), invalidJSON, 0600) + require.NoError(t, err) + + // Update test case to use temp file + for i := range tests { + if tests[i].path == "/tmp/invalid.json" { + tests[i].path = tempFile.Name() + break + } + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := importerService.ImportFromPath(context.Background(), tt.path) + + if tt.expectError { + assert.Error(t, err) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg) + } + } else { + assert.NoError(t, err) + } + }) + } } diff --git a/internal/service/registry_service.go b/internal/service/registry_service.go index b6dbe56..f499d42 100644 --- a/internal/service/registry_service.go +++ b/internal/service/registry_service.go @@ -6,12 +6,12 @@ import ( "fmt" "time" - "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/modelcontextprotocol/registry/internal/config" "github.com/modelcontextprotocol/registry/internal/database" "github.com/modelcontextprotocol/registry/internal/validators" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/modelcontextprotocol/registry/pkg/model" ) const maxServerVersionsPerServer = 10000 @@ -30,219 +30,136 @@ func NewRegistryService(db database.Database, cfg *config.Config) RegistryServic } } -// List returns registry entries with cursor-based pagination and optional filtering -func (s *registryServiceImpl) List(filter *database.ServerFilter, cursor string, limit int) ([]apiv0.ServerJSON, string, error) { - // Create a timeout context for the database operation - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - +// ListServers returns registry entries with cursor-based pagination and optional filtering +func (s *registryServiceImpl) ListServers(ctx context.Context, filter *database.ServerFilter, cursor string, limit int) ([]*apiv0.ServerResponse, string, error) { // If limit is not set or negative, use a default limit if limit <= 0 { limit = 30 } // Use the database's ListServers method with pagination and filtering - serverRecords, nextCursor, err := s.db.List(ctx, nil, filter, cursor, limit) + serverRecords, nextCursor, err := s.db.ListServers(ctx, nil, filter, cursor, limit) if err != nil { return nil, "", err } - // Return ServerJSONs directly - result := make([]apiv0.ServerJSON, len(serverRecords)) - for i, record := range serverRecords { - result[i] = *record - } - - return result, nextCursor, nil + return serverRecords, nextCursor, nil } -// GetByVersionID retrieves a specific server by its registry metadata version ID -func (s *registryServiceImpl) GetByVersionID(versionID string) (*apiv0.ServerJSON, error) { - // Create a timeout context for the database operation - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - serverRecord, err := s.db.GetByVersionID(ctx, nil, versionID) +// GetServerByName retrieves the latest version of a server by its server name +func (s *registryServiceImpl) GetServerByName(ctx context.Context, serverName string) (*apiv0.ServerResponse, error) { + serverRecord, err := s.db.GetServerByName(ctx, nil, serverName) if err != nil { return nil, err } - // Return the server record directly return serverRecord, nil } -// GetByServerID retrieves the latest version of a server by its server ID -func (s *registryServiceImpl) GetByServerID(serverID string) (*apiv0.ServerJSON, error) { - // Create a timeout context for the database operation - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - serverRecord, err := s.db.GetByServerID(ctx, nil, serverID) +// GetServerByNameAndVersion retrieves a specific version of a server by server name and version +func (s *registryServiceImpl) GetServerByNameAndVersion(ctx context.Context, serverName string, version string) (*apiv0.ServerResponse, error) { + serverRecord, err := s.db.GetServerByNameAndVersion(ctx, nil, serverName, version) if err != nil { return nil, err } - // Return the server record directly return serverRecord, nil } -// GetByServerIDAndVersion retrieves a specific version of a server by server ID and version -func (s *registryServiceImpl) GetByServerIDAndVersion(serverID string, version string) (*apiv0.ServerJSON, error) { - // Create a timeout context for the database operation - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - serverRecord, err := s.db.GetByServerIDAndVersion(ctx, nil, serverID, version) +// GetAllVersionsByServerName retrieves all versions of a server by server name +func (s *registryServiceImpl) GetAllVersionsByServerName(ctx context.Context, serverName string) ([]*apiv0.ServerResponse, error) { + serverRecords, err := s.db.GetAllVersionsByServerName(ctx, nil, serverName) if err != nil { return nil, err } - // Return the server record directly - return serverRecord, nil + return serverRecords, nil } -// GetAllVersionsByServerID retrieves all versions of a server by server ID -func (s *registryServiceImpl) GetAllVersionsByServerID(serverID string) ([]apiv0.ServerJSON, error) { - // Create a timeout context for the database operation - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - serverRecords, err := s.db.GetAllVersionsByServerID(ctx, nil, serverID) - if err != nil { - return nil, err - } - - // Return ServerJSONs directly - result := make([]apiv0.ServerJSON, len(serverRecords)) - for i, record := range serverRecords { - result[i] = *record - } - - return result, nil -} - -// Publish publishes a server with flattened _meta extensions -func (s *registryServiceImpl) Publish(req apiv0.ServerJSON) (*apiv0.ServerJSON, error) { - // Create a timeout context for the database operation - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Validate the request - if err := validators.ValidatePublishRequest(req, s.cfg); err != nil { - return nil, err - } - - result, err := database.InTransactionT(ctx, s.db, func(txCtx context.Context, tx pgx.Tx) (*apiv0.ServerJSON, error) { - publishTime := time.Now() - serverJSON := req - - // Acquire advisory lock to prevent concurrent publishes of the same server - if err := s.db.AcquirePublishLock(txCtx, tx, serverJSON.Name); err != nil { - return nil, err - } - - // Check for duplicate remote URLs - if err := s.validateNoDuplicateRemoteURLs(txCtx, tx, serverJSON); err != nil { - return nil, err - } - - filter := &database.ServerFilter{Name: &serverJSON.Name} - existingServerVersions, _, err := s.db.List(txCtx, tx, filter, "", maxServerVersionsPerServer) - if err != nil && !errors.Is(err, database.ErrNotFound) { - return nil, err - } - - // Check we haven't exceeded the maximum versions allowed for a server - if len(existingServerVersions) >= maxServerVersionsPerServer { - return nil, database.ErrMaxServersReached - } - - // Check this isn't a duplicate version - for _, server := range existingServerVersions { - existingVersion := server.Version - if existingVersion == serverJSON.Version { - return nil, database.ErrInvalidVersion - } - } - - // Determine if this version should be marked as latest - existingLatest := s.getCurrentLatestVersion(existingServerVersions) - isNewLatest := true - if existingLatest != nil { - var existingPublishedAt time.Time - if existingLatest.Meta != nil && existingLatest.Meta.Official != nil { - existingPublishedAt = existingLatest.Meta.Official.PublishedAt - } - isNewLatest = CompareVersions( - serverJSON.Version, - existingLatest.Version, - publishTime, - existingPublishedAt, - ) > 0 - } - - // Unmark old latest version if needed - if isNewLatest && existingLatest != nil { - if existingLatest.Meta != nil && existingLatest.Meta.Official != nil { - existingLatest.Meta.Official.IsLatest = false - if _, err := s.db.UpdateServer(txCtx, tx, existingLatest.Meta.Official.VersionID, existingLatest); err != nil { - return nil, err - } - } - } - - // Create complete server with metadata - server := s.createServerWithMetadata(serverJSON, existingServerVersions, publishTime, isNewLatest) - - // Insert new server version - return s.db.CreateServer(txCtx, tx, &server) +// CreateServer creates a new server version +func (s *registryServiceImpl) CreateServer(ctx context.Context, req *apiv0.ServerJSON) (*apiv0.ServerResponse, error) { + // Wrap the entire operation in a transaction + return database.InTransactionT(ctx, s.db, func(ctx context.Context, tx pgx.Tx) (*apiv0.ServerResponse, error) { + return s.createServerInTransaction(ctx, tx, req) }) +} - if err != nil { +// createServerInTransaction contains the actual CreateServer logic within a transaction +func (s *registryServiceImpl) createServerInTransaction(ctx context.Context, tx pgx.Tx, req *apiv0.ServerJSON) (*apiv0.ServerResponse, error) { + // Validate the request + if err := validators.ValidatePublishRequest(ctx, *req, s.cfg); err != nil { return nil, err } - return result, nil -} + publishTime := time.Now() + serverJSON := *req -// createServerWithMetadata creates a server with proper metadata including server_id and version_id -func (s *registryServiceImpl) createServerWithMetadata( - serverJSON apiv0.ServerJSON, - existingServerVersions []*apiv0.ServerJSON, - publishTime time.Time, - isNewLatest bool, -) apiv0.ServerJSON { - server := serverJSON // Copy the input - - // Initialize meta if not present - if server.Meta == nil { - server.Meta = &apiv0.ServerMeta{} + // Acquire advisory lock to prevent concurrent publishes of the same server + if err := s.db.AcquirePublishLock(ctx, tx, serverJSON.Name); err != nil { + return nil, err } - // Determine server_id - either from existing versions or generate new one - var serverID string - if len(existingServerVersions) > 0 { - // Use existing server_id from any existing version - firstExisting := existingServerVersions[0] - if firstExisting.Meta != nil && firstExisting.Meta.Official != nil { - serverID = firstExisting.Meta.Official.ServerID + // Check for duplicate remote URLs + if err := s.validateNoDuplicateRemoteURLs(ctx, tx, serverJSON); err != nil { + return nil, err + } + + // Check we haven't exceeded the maximum versions allowed for a server + versionCount, err := s.db.CountServerVersions(ctx, tx, serverJSON.Name) + if err != nil && !errors.Is(err, database.ErrNotFound) { + return nil, err + } + if versionCount >= maxServerVersionsPerServer { + return nil, database.ErrMaxServersReached + } + + // Check this isn't a duplicate version + versionExists, err := s.db.CheckVersionExists(ctx, tx, serverJSON.Name, serverJSON.Version) + if err != nil { + return nil, err + } + if versionExists { + return nil, database.ErrInvalidVersion + } + + // Get current latest version to determine if new version should be latest + currentLatest, err := s.db.GetCurrentLatestVersion(ctx, tx, serverJSON.Name) + if err != nil && !errors.Is(err, database.ErrNotFound) { + return nil, err + } + + // Determine if this version should be marked as latest + isNewLatest := true + if currentLatest != nil { + var existingPublishedAt time.Time + if currentLatest.Meta.Official != nil { + existingPublishedAt = currentLatest.Meta.Official.PublishedAt + } + isNewLatest = CompareVersions( + serverJSON.Version, + currentLatest.Server.Version, + publishTime, + existingPublishedAt, + ) > 0 + } + + // Unmark old latest version if needed + if isNewLatest && currentLatest != nil { + if err := s.db.UnmarkAsLatest(ctx, tx, serverJSON.Name); err != nil { + return nil, err } } - if serverID == "" { - // This is the first version of a new server - serverID = uuid.New().String() - } - // Set registry metadata - server.Meta.Official = &apiv0.RegistryExtensions{ - ServerID: serverID, - VersionID: uuid.New().String(), + // Create metadata for the new server + officialMeta := &apiv0.RegistryExtensions{ + Status: model.StatusActive, /* New versions are active by default */ PublishedAt: publishTime, UpdatedAt: publishTime, IsLatest: isNewLatest, } - return server + // Insert new server version + return s.db.CreateServer(ctx, tx, &serverJSON, officialMeta) } // validateNoDuplicateRemoteURLs checks that no other server is using the same remote URLs @@ -252,15 +169,15 @@ func (s *registryServiceImpl) validateNoDuplicateRemoteURLs(ctx context.Context, // Use filter to find servers with this remote URL filter := &database.ServerFilter{RemoteURL: &remote.URL} - conflictingServers, _, err := s.db.List(ctx, tx, filter, "", 1000) + conflictingServers, _, err := s.db.ListServers(ctx, tx, filter, "", 1000) if err != nil { return fmt.Errorf("failed to check remote URL conflict: %w", err) } // Check if any conflicting server has a different name for _, conflictingServer := range conflictingServers { - if conflictingServer.Name != serverDetail.Name { - return fmt.Errorf("remote URL %s is already used by server %s", remote.URL, conflictingServer.Name) + if conflictingServer.Server.Name != serverDetail.Name { + return fmt.Errorf("remote URL %s is already used by server %s", remote.URL, conflictingServer.Server.Name) } } } @@ -268,62 +185,83 @@ func (s *registryServiceImpl) validateNoDuplicateRemoteURLs(ctx context.Context, return nil } -// getCurrentLatestVersion finds the current latest version from existing server versions -func (s *registryServiceImpl) getCurrentLatestVersion(existingServerVersions []*apiv0.ServerJSON) *apiv0.ServerJSON { - for _, server := range existingServerVersions { - if server.Meta != nil && server.Meta.Official != nil && - server.Meta.Official.IsLatest { - return server - } - } - return nil +// UpdateServer updates an existing server with new details +func (s *registryServiceImpl) UpdateServer(ctx context.Context, serverName, version string, req *apiv0.ServerJSON, newStatus *string) (*apiv0.ServerResponse, error) { + // Wrap the entire operation in a transaction + return database.InTransactionT(ctx, s.db, func(ctx context.Context, tx pgx.Tx) (*apiv0.ServerResponse, error) { + return s.updateServerInTransaction(ctx, tx, serverName, version, req, newStatus) + }) } -// EditServer updates an existing server with new details (admin operation) -func (s *registryServiceImpl) EditServer(versionID string, req apiv0.ServerJSON) (*apiv0.ServerJSON, error) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Validate the request - if err := validators.ValidatePublishRequest(req, s.cfg); err != nil { +// updateServerInTransaction contains the actual UpdateServer logic within a transaction +func (s *registryServiceImpl) updateServerInTransaction(ctx context.Context, tx pgx.Tx, serverName, version string, req *apiv0.ServerJSON, newStatus *string) (*apiv0.ServerResponse, error) { + // Get current server to check if it's deleted or being deleted + currentServer, err := s.db.GetServerByNameAndVersion(ctx, tx, serverName, version) + if err != nil { return nil, err } - return database.InTransactionT(ctx, s.db, func(txCtx context.Context, tx pgx.Tx) (*apiv0.ServerJSON, error) { - // Get the current server to preserve metadata - currentServer, err := s.db.GetByVersionID(txCtx, tx, versionID) + // Skip registry validation if: + // 1. Server is currently deleted, OR + // 2. Server is being set to deleted status + currentlyDeleted := currentServer.Meta.Official != nil && currentServer.Meta.Official.Status == model.StatusDeleted + beingDeleted := newStatus != nil && *newStatus == string(model.StatusDeleted) + skipRegistryValidation := currentlyDeleted || beingDeleted + + // Validate the request, potentially skipping registry validation for deleted servers + if err := s.validateUpdateRequest(ctx, *req, skipRegistryValidation); err != nil { + return nil, err + } + + // Acquire advisory lock to prevent concurrent edits of servers with same name + if err := s.db.AcquirePublishLock(ctx, tx, serverName); err != nil { + return nil, err + } + + // Merge the request with the current server, preserving metadata + updatedServer := *req + + // Check for duplicate remote URLs using the updated server + if err := s.validateNoDuplicateRemoteURLs(ctx, tx, updatedServer); err != nil { + return nil, err + } + + // Update server in database + updatedServerResponse, err := s.db.UpdateServer(ctx, tx, serverName, version, &updatedServer) + if err != nil { + return nil, err + } + + // Handle status change if provided + if newStatus != nil { + updatedWithStatus, err := s.db.SetServerStatus(ctx, tx, serverName, version, *newStatus) if err != nil { return nil, err } + return updatedWithStatus, nil + } - // Acquire advisory lock to prevent concurrent edits of servers with same name - if err := s.db.AcquirePublishLock(txCtx, tx, currentServer.Name); err != nil { - return nil, err - } - - // Merge the request with the current server, preserving metadata - updatedServer := *currentServer + return updatedServerResponse, nil +} - // Update only the user-modifiable fields from the request - updatedServer.Name = req.Name - updatedServer.Description = req.Description - updatedServer.Version = req.Version - updatedServer.Status = req.Status - updatedServer.Repository = req.Repository - updatedServer.Remotes = req.Remotes - updatedServer.Packages = req.Packages +// validateUpdateRequest validates an update request with optional registry validation skipping +func (s *registryServiceImpl) validateUpdateRequest(ctx context.Context, req apiv0.ServerJSON, skipRegistryValidation bool) error { + // Always validate the server JSON structure + if err := validators.ValidateServerJSON(&req); err != nil { + return err + } - // Update the UpdatedAt timestamp in metadata - if updatedServer.Meta != nil && updatedServer.Meta.Official != nil { - updatedServer.Meta.Official.UpdatedAt = time.Now() - } + // Skip registry validation if requested (for deleted servers) + if skipRegistryValidation || !s.cfg.EnableRegistryValidation { + return nil + } - // Check for duplicate remote URLs using the updated server - if err := s.validateNoDuplicateRemoteURLs(txCtx, tx, updatedServer); err != nil { - return nil, err + // Perform registry validation for all packages + for i, pkg := range req.Packages { + if err := validators.ValidatePackage(ctx, pkg, req.Name); err != nil { + return fmt.Errorf("registry validation failed for package %d (%s): %w", i, pkg.Identifier, err) } + } - // Update server in database - return s.db.UpdateServer(txCtx, tx, versionID, &updatedServer) - }) + return nil } diff --git a/internal/service/registry_service_test.go b/internal/service/registry_service_test.go index 29743f8..927cba4 100644 --- a/internal/service/registry_service_test.go +++ b/internal/service/registry_service_test.go @@ -6,15 +6,19 @@ import ( "fmt" "sync" "testing" + "time" "github.com/modelcontextprotocol/registry/internal/config" "github.com/modelcontextprotocol/registry/internal/database" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestValidateNoDuplicateRemoteURLs(t *testing.T) { + ctx := context.Background() + // Create test data existingServers := map[string]*apiv0.ServerJSON{ "existing1": { @@ -39,11 +43,10 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { testDB := database.NewTestDB(t) service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) + // Create existing servers using the new CreateServer method for _, server := range existingServers { - _, err := service.Publish(*server) - if err != nil { - t.Fatalf("failed to publish server: %v", err) - } + _, err := service.CreateServer(ctx, server) + require.NoError(t, err, "failed to create server: %v", err) } tests := []struct { @@ -65,7 +68,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { { name: "new unique remote URLs - should pass", serverDetail: apiv0.ServerJSON{ - Name: "com.example/new-server", + Name: "com.example/new-server-unique", Description: "A new server", Version: "1.0.0", Remotes: []model.Transport{ @@ -78,7 +81,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { { name: "duplicate remote URL - should fail", serverDetail: apiv0.ServerJSON{ - Name: "com.example/new-server", + Name: "com.example/new-server-duplicate", Description: "A new server with duplicate URL", Version: "1.0.0", Remotes: []model.Transport{ @@ -93,7 +96,7 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { serverDetail: apiv0.ServerJSON{ Name: "com.example/existing-server", // Same name as existing Description: "Updated existing server", - Version: "1.1.0", + Version: "1.1.0", // Different version Remotes: []model.Transport{ {Type: "streamable-http", URL: "https://api.example.com/mcp"}, // Same URL as before }, @@ -104,7 +107,6 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() impl := service.(*registryServiceImpl) err := impl.validateNoDuplicateRemoteURLs(ctx, nil, tt.serverDetail) @@ -119,59 +121,55 @@ func TestValidateNoDuplicateRemoteURLs(t *testing.T) { } } -func TestGetByServerID(t *testing.T) { +func TestGetServerByName(t *testing.T) { + ctx := context.Background() testDB := database.NewTestDB(t) service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) - // Publish multiple versions of the same server - server1, err := service.Publish(apiv0.ServerJSON{ + // Create multiple versions of the same server + _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ Name: "com.example/test-server", Description: "Test server v1", Version: "1.0.0", }) - assert.NoError(t, err) + require.NoError(t, err) - _, err = service.Publish(apiv0.ServerJSON{ + _, err = service.CreateServer(ctx, &apiv0.ServerJSON{ Name: "com.example/test-server", Description: "Test server v2", Version: "2.0.0", }) - assert.NoError(t, err) + require.NoError(t, err) tests := []struct { name string - serverID string + serverName string expectError bool errorMsg string - checkResult func(*testing.T, *apiv0.ServerJSON) + checkResult func(*testing.T, *apiv0.ServerResponse) }{ { - name: "get latest version by server ID", - serverID: server1.Meta.Official.ServerID, + name: "get latest version by server name", + serverName: "com.example/test-server", expectError: false, - checkResult: func(t *testing.T, result *apiv0.ServerJSON) { + checkResult: func(t *testing.T, result *apiv0.ServerResponse) { t.Helper() - assert.Equal(t, "2.0.0", result.Version) // Should get latest version - assert.Equal(t, "Test server v2", result.Description) + assert.Equal(t, "2.0.0", result.Server.Version) // Should get latest version + assert.Equal(t, "Test server v2", result.Server.Description) assert.True(t, result.Meta.Official.IsLatest) }, }, { name: "server not found", - serverID: "00000000-0000-0000-0000-000000000000", + serverName: "com.example/non-existent", expectError: true, errorMsg: "record not found", }, - { - name: "invalid server ID format", - serverID: "invalid-uuid", - expectError: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := service.GetByServerID(tt.serverID) + result, err := service.GetServerByName(ctx, tt.serverName) if tt.expectError { assert.Error(t, err) @@ -190,82 +188,79 @@ func TestGetByServerID(t *testing.T) { } } -func TestGetByServerIDAndVersion(t *testing.T) { +func TestGetServerByNameAndVersion(t *testing.T) { + ctx := context.Background() testDB := database.NewTestDB(t) service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) - // Publish multiple versions of the same server - server1, err := service.Publish(apiv0.ServerJSON{ - Name: "com.example/versioned-server", + serverName := "com.example/versioned-server" + + // Create multiple versions of the same server + _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, Description: "Versioned server v1", Version: "1.0.0", }) - assert.NoError(t, err) + require.NoError(t, err) - _, err = service.Publish(apiv0.ServerJSON{ - Name: "com.example/versioned-server", + _, err = service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, Description: "Versioned server v2", Version: "2.0.0", }) - assert.NoError(t, err) + require.NoError(t, err) tests := []struct { name string - serverID string + serverName string version string expectError bool errorMsg string - checkResult func(*testing.T, *apiv0.ServerJSON) + checkResult func(*testing.T, *apiv0.ServerResponse) }{ { name: "get specific version 1.0.0", - serverID: server1.Meta.Official.ServerID, + serverName: serverName, version: "1.0.0", expectError: false, - checkResult: func(t *testing.T, result *apiv0.ServerJSON) { + checkResult: func(t *testing.T, result *apiv0.ServerResponse) { t.Helper() - assert.Equal(t, "1.0.0", result.Version) - assert.Equal(t, "Versioned server v1", result.Description) + assert.Equal(t, "1.0.0", result.Server.Version) + assert.Equal(t, "Versioned server v1", result.Server.Description) assert.False(t, result.Meta.Official.IsLatest) }, }, { name: "get specific version 2.0.0", - serverID: server1.Meta.Official.ServerID, + serverName: serverName, version: "2.0.0", expectError: false, - checkResult: func(t *testing.T, result *apiv0.ServerJSON) { + checkResult: func(t *testing.T, result *apiv0.ServerResponse) { t.Helper() - assert.Equal(t, "2.0.0", result.Version) - assert.Equal(t, "Versioned server v2", result.Description) + assert.Equal(t, "2.0.0", result.Server.Version) + assert.Equal(t, "Versioned server v2", result.Server.Description) assert.True(t, result.Meta.Official.IsLatest) }, }, { name: "version not found", - serverID: server1.Meta.Official.ServerID, + serverName: serverName, version: "3.0.0", expectError: true, errorMsg: "record not found", }, { name: "server not found", - serverID: "00000000-0000-0000-0000-000000000000", + serverName: "com.example/non-existent", version: "1.0.0", expectError: true, errorMsg: "record not found", }, - { - name: "invalid server ID format", - serverID: "invalid-uuid", - version: "1.0.0", - expectError: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := service.GetByServerIDAndVersion(tt.serverID, tt.version) + result, err := service.GetServerByNameAndVersion(ctx, tt.serverName, tt.version) if tt.expectError { assert.Error(t, err) @@ -284,44 +279,47 @@ func TestGetByServerIDAndVersion(t *testing.T) { } } -func TestGetAllVersionsByServerID(t *testing.T) { +func TestGetAllVersionsByServerName(t *testing.T) { + ctx := context.Background() testDB := database.NewTestDB(t) service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) - // Publish multiple versions of the same server - server1, err := service.Publish(apiv0.ServerJSON{ - Name: "com.example/multi-version-server", + serverName := "com.example/multi-version-server" + + // Create multiple versions of the same server + _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, Description: "Multi-version server v1", Version: "1.0.0", }) - assert.NoError(t, err) + require.NoError(t, err) - _, err = service.Publish(apiv0.ServerJSON{ - Name: "com.example/multi-version-server", + _, err = service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, Description: "Multi-version server v2", Version: "2.0.0", }) - assert.NoError(t, err) + require.NoError(t, err) - _, err = service.Publish(apiv0.ServerJSON{ - Name: "com.example/multi-version-server", + _, err = service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, Description: "Multi-version server v2.1", Version: "2.1.0", }) - assert.NoError(t, err) + require.NoError(t, err) tests := []struct { name string - serverID string + serverName string expectError bool errorMsg string - checkResult func(*testing.T, []apiv0.ServerJSON) + checkResult func(*testing.T, []*apiv0.ServerResponse) }{ { name: "get all versions of server", - serverID: server1.Meta.Official.ServerID, + serverName: serverName, expectError: false, - checkResult: func(t *testing.T, result []apiv0.ServerJSON) { + checkResult: func(t *testing.T, result []*apiv0.ServerResponse) { t.Helper() assert.Len(t, result, 3) @@ -329,9 +327,8 @@ func TestGetAllVersionsByServerID(t *testing.T) { versions := make([]string, 0, len(result)) latestCount := 0 for _, server := range result { - versions = append(versions, server.Version) - assert.Equal(t, server1.Meta.Official.ServerID, server.Meta.Official.ServerID) - assert.Equal(t, "com.example/multi-version-server", server.Name) + versions = append(versions, server.Server.Version) + assert.Equal(t, serverName, server.Server.Name) if server.Meta.Official.IsLatest { latestCount++ } @@ -348,20 +345,15 @@ func TestGetAllVersionsByServerID(t *testing.T) { }, { name: "server not found", - serverID: "00000000-0000-0000-0000-000000000000", + serverName: "com.example/non-existent", expectError: true, errorMsg: "record not found", }, - { - name: "invalid server ID format", - serverID: "invalid-uuid", - expectError: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := service.GetAllVersionsByServerID(tt.serverID) + result, err := service.GetAllVersionsByServerName(ctx, tt.serverName) if tt.expectError { assert.Error(t, err) @@ -380,21 +372,23 @@ func TestGetAllVersionsByServerID(t *testing.T) { } } -func TestPublishConcurrentVersionsNoRace(t *testing.T) { +func TestCreateServerConcurrentVersionsNoRace(t *testing.T) { + ctx := context.Background() testDB := database.NewTestDB(t) service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) const concurrency = 100 - results := make([]*apiv0.ServerJSON, concurrency) + serverName := "com.example/test-concurrent" + results := make([]*apiv0.ServerResponse, concurrency) errors := make([]error, concurrency) var wg sync.WaitGroup - for i := range concurrency { + for i := 0; i < concurrency; i++ { wg.Add(1) go func(idx int) { defer wg.Done() - result, err := service.Publish(apiv0.ServerJSON{ - Name: "com.example/test-concurrent", + result, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, Description: fmt.Sprintf("Version %d", idx), Version: fmt.Sprintf("1.0.%d", idx), }) @@ -404,40 +398,372 @@ func TestPublishConcurrentVersionsNoRace(t *testing.T) { } wg.Wait() + // All publishes should succeed for i, err := range errors { - assert.NoError(t, err, "publish %d failed", i) + assert.NoError(t, err, "create server %d failed", i) } - var sharedServerID string + // All results should have the same server name for i, result := range results { - if result != nil && result.Meta != nil && result.Meta.Official != nil { - if sharedServerID == "" { - sharedServerID = result.Meta.Official.ServerID - } - assert.Equal(t, sharedServerID, result.Meta.Official.ServerID, - "version %d has different serverID", i) + if result != nil { + assert.Equal(t, serverName, result.Server.Name, "version %d has different server name", i) } } - // Query database to check the final state after all publishes complete - // This is necessary because the returned results reflect state at publish time, - // but concurrent publishes may have updated the latest flag since then - filter := &database.ServerFilter{Name: strPtr("com.example/test-concurrent")} - dbResults, _, err := testDB.List(context.Background(), nil, filter, "", 1000) - assert.NoError(t, err, "failed to query database") + // Query database to check the final state after all creates complete + allVersions, err := service.GetAllVersionsByServerName(ctx, serverName) + require.NoError(t, err, "failed to get all versions") latestCount := 0 var latestVersion string - for _, r := range dbResults { - if r.Meta != nil && r.Meta.Official != nil && r.Meta.Official.IsLatest { + for _, r := range allVersions { + if r.Meta.Official.IsLatest { latestCount++ - latestVersion = r.Version + latestVersion = r.Server.Version } } assert.Equal(t, 1, latestCount, "should have exactly one latest version in database, found version: %s", latestVersion) + assert.Len(t, allVersions, concurrency, "should have all %d versions", concurrency) } -func strPtr(s string) *string { - return &s +func TestUpdateServer(t *testing.T) { + ctx := context.Background() + testDB := database.NewTestDB(t) + service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) + + serverName := "com.example/update-test-server" + version := "1.0.0" + + // Create initial server + _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, + Description: "Original description", + Version: version, + Remotes: []model.Transport{ + {Type: "streamable-http", URL: "https://original.example.com/mcp"}, + }, + }) + require.NoError(t, err) + + tests := []struct { + name string + serverName string + version string + updatedServer *apiv0.ServerJSON + newStatus *string + expectError bool + errorMsg string + checkResult func(*testing.T, *apiv0.ServerResponse) + }{ + { + name: "successful server update", + serverName: serverName, + version: version, + updatedServer: &apiv0.ServerJSON{ + Name: serverName, + Description: "Updated description", + Version: version, + Remotes: []model.Transport{ + {Type: "streamable-http", URL: "https://updated.example.com/mcp"}, + }, + }, + expectError: false, + checkResult: func(t *testing.T, result *apiv0.ServerResponse) { + t.Helper() + assert.Equal(t, "Updated description", result.Server.Description) + assert.Len(t, result.Server.Remotes, 1) + assert.Equal(t, "https://updated.example.com/mcp", result.Server.Remotes[0].URL) + assert.NotZero(t, result.Meta.Official.UpdatedAt) + }, + }, + { + name: "update with status change", + serverName: serverName, + version: version, + updatedServer: &apiv0.ServerJSON{ + Name: serverName, + Description: "Updated with status change", + Version: version, + }, + newStatus: stringPtr(string(model.StatusDeprecated)), + expectError: false, + checkResult: func(t *testing.T, result *apiv0.ServerResponse) { + t.Helper() + assert.Equal(t, "Updated with status change", result.Server.Description) + assert.Equal(t, model.StatusDeprecated, result.Meta.Official.Status) + }, + }, + { + name: "update non-existent server", + serverName: "com.example/non-existent", + version: "1.0.0", + updatedServer: &apiv0.ServerJSON{ + Name: "com.example/non-existent", + Description: "Should fail", + Version: "1.0.0", + }, + expectError: true, + errorMsg: "record not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := service.UpdateServer(ctx, tt.serverName, tt.version, tt.updatedServer, tt.newStatus) + + if tt.expectError { + assert.Error(t, err) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg) + } + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + if tt.checkResult != nil { + tt.checkResult(t, result) + } + } + }) + } } + +func TestUpdateServer_SkipValidationForDeletedServers(t *testing.T) { + ctx := context.Background() + testDB := database.NewTestDB(t) + // Enable registry validation to test that it gets skipped for deleted servers + service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: true}) + + serverName := "com.example/validation-skip-test" + version := "1.0.0" + + // Create server with invalid package configuration (this would fail registry validation) + invalidServer := &apiv0.ServerJSON{ + Name: serverName, + Description: "Server with invalid package for testing validation skip", + Version: version, + Packages: []model.Package{ + { + RegistryType: "npm", + Identifier: "non-existent-package-for-validation-test", + Version: "1.0.0", + Transport: model.Transport{Type: "stdio"}, + }, + }, + } + + // Create initial server (validation disabled for creation in this test) + originalConfig := service.(*registryServiceImpl).cfg.EnableRegistryValidation + service.(*registryServiceImpl).cfg.EnableRegistryValidation = false + _, err := service.CreateServer(ctx, invalidServer) + require.NoError(t, err, "failed to create server with validation disabled") + service.(*registryServiceImpl).cfg.EnableRegistryValidation = originalConfig + + // First, set server to deleted status + deletedStatus := string(model.StatusDeleted) + _, err = service.UpdateServer(ctx, serverName, version, invalidServer, &deletedStatus) + require.NoError(t, err, "should be able to set server to deleted (validation should be skipped)") + + // Verify server is now deleted + updatedServer, err := service.GetServerByNameAndVersion(ctx, serverName, version) + require.NoError(t, err) + assert.Equal(t, model.StatusDeleted, updatedServer.Meta.Official.Status) + + // Now try to update a deleted server - validation should be skipped + updatedInvalidServer := &apiv0.ServerJSON{ + Name: serverName, + Description: "Updated description for deleted server", + Version: version, + Packages: []model.Package{ + { + RegistryType: "npm", + Identifier: "another-non-existent-package-for-validation-test", + Version: "2.0.0", + Transport: model.Transport{Type: "stdio"}, + }, + }, + } + + // This should succeed despite invalid packages because server is deleted + result, err := service.UpdateServer(ctx, serverName, version, updatedInvalidServer, nil) + assert.NoError(t, err, "updating deleted server should skip registry validation") + assert.NotNil(t, result) + assert.Equal(t, "Updated description for deleted server", result.Server.Description) + assert.Equal(t, model.StatusDeleted, result.Meta.Official.Status) + + // Test updating a server being set to deleted status + activeServer := &apiv0.ServerJSON{ + Name: "com.example/being-deleted-test", + Description: "Server being deleted", + Version: "1.0.0", + Packages: []model.Package{ + { + RegistryType: "npm", + Identifier: "yet-another-non-existent-package", + Version: "1.0.0", + Transport: model.Transport{Type: "stdio"}, + }, + }, + } + + // Create active server (with validation disabled) + service.(*registryServiceImpl).cfg.EnableRegistryValidation = false + _, err = service.CreateServer(ctx, activeServer) + require.NoError(t, err) + service.(*registryServiceImpl).cfg.EnableRegistryValidation = originalConfig + + // Update server and set to deleted in same operation - should skip validation + newDeletedStatus := string(model.StatusDeleted) + result2, err := service.UpdateServer(ctx, "com.example/being-deleted-test", "1.0.0", activeServer, &newDeletedStatus) + assert.NoError(t, err, "updating server being set to deleted should skip registry validation") + assert.NotNil(t, result2) + assert.Equal(t, model.StatusDeleted, result2.Meta.Official.Status) +} + +func TestListServers(t *testing.T) { + ctx := context.Background() + testDB := database.NewTestDB(t) + service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) + + // Create test servers + testServers := []struct { + name string + version string + description string + }{ + {"com.example/server-alpha", "1.0.0", "Alpha server"}, + {"com.example/server-beta", "1.0.0", "Beta server"}, + {"com.example/server-gamma", "2.0.0", "Gamma server"}, + } + + for _, server := range testServers { + _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: server.name, + Description: server.description, + Version: server.version, + }) + require.NoError(t, err) + } + + tests := []struct { + name string + filter *database.ServerFilter + cursor string + limit int + expectedCount int + expectError bool + }{ + { + name: "list all servers", + filter: nil, + limit: 10, + expectedCount: 3, + }, + { + name: "filter by name", + filter: &database.ServerFilter{ + Name: stringPtr("com.example/server-alpha"), + }, + limit: 10, + expectedCount: 1, + }, + { + name: "filter by version", + filter: &database.ServerFilter{ + Version: stringPtr("1.0.0"), + }, + limit: 10, + expectedCount: 2, + }, + { + name: "pagination with limit", + filter: nil, + limit: 2, + expectedCount: 2, + }, + { + name: "cursor pagination", + filter: nil, + cursor: "com.example/server-alpha", + limit: 10, + // Should return servers after 'server-alpha' alphabetically + expectedCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results, nextCursor, err := service.ListServers(ctx, tt.filter, tt.cursor, tt.limit) + + if tt.expectError { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Len(t, results, tt.expectedCount) + + // Test cursor behavior + if tt.limit < len(testServers) && len(results) == tt.limit { + assert.NotEmpty(t, nextCursor, "Should return next cursor when results are limited") + } + }) + } +} + +func TestVersionComparison(t *testing.T) { + ctx := context.Background() + testDB := database.NewTestDB(t) + service := NewRegistryService(testDB, &config.Config{EnableRegistryValidation: false}) + + serverName := "com.example/version-comparison-server" + + // Create versions in non-chronological order to test version comparison logic + versions := []struct { + version string + description string + delay time.Duration // Delay to simulate different publish times + }{ + {"2.0.0", "Version 2.0.0", 0}, + {"1.0.0", "Version 1.0.0", 10 * time.Millisecond}, + {"2.1.0", "Version 2.1.0", 20 * time.Millisecond}, + {"1.5.0", "Version 1.5.0", 30 * time.Millisecond}, + } + + for _, v := range versions { + if v.delay > 0 { + time.Sleep(v.delay) + } + _, err := service.CreateServer(ctx, &apiv0.ServerJSON{ + Name: serverName, + Description: v.description, + Version: v.version, + }) + require.NoError(t, err, "Failed to create version %s", v.version) + } + + // Get the latest version - should be 2.1.0 based on semantic versioning + latest, err := service.GetServerByName(ctx, serverName) + require.NoError(t, err) + + assert.Equal(t, "2.1.0", latest.Server.Version, "Latest version should be 2.1.0") + assert.True(t, latest.Meta.Official.IsLatest) + + // Verify only one version is marked as latest + allVersions, err := service.GetAllVersionsByServerName(ctx, serverName) + require.NoError(t, err) + + latestCount := 0 + for _, version := range allVersions { + if version.Meta.Official.IsLatest { + latestCount++ + } + } + assert.Equal(t, 1, latestCount, "Exactly one version should be marked as latest") +} + +// Helper functions +func stringPtr(s string) *string { + return &s +} \ No newline at end of file diff --git a/internal/service/service.go b/internal/service/service.go index dc6a536..4ebd8ec 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,24 +1,24 @@ package service import ( + "context" + "github.com/modelcontextprotocol/registry/internal/database" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" ) // RegistryService defines the interface for registry operations type RegistryService interface { - // Retrieve all servers with optional filtering - List(filter *database.ServerFilter, cursor string, limit int) ([]apiv0.ServerJSON, string, error) - // Retrieve a single server by registry metadata version ID - GetByVersionID(versionID string) (*apiv0.ServerJSON, error) - // Retrieve latest version of a server by server ID - GetByServerID(serverID string) (*apiv0.ServerJSON, error) - // Retrieve specific version of a server by server ID and version - GetByServerIDAndVersion(serverID string, version string) (*apiv0.ServerJSON, error) - // Retrieve all versions of a server by server ID - GetAllVersionsByServerID(serverID string) ([]apiv0.ServerJSON, error) - // Publish a server - Publish(req apiv0.ServerJSON) (*apiv0.ServerJSON, error) - // Update an existing server - EditServer(id string, req apiv0.ServerJSON) (*apiv0.ServerJSON, error) + // ListServers retrieve all servers with optional filtering + ListServers(ctx context.Context, filter *database.ServerFilter, cursor string, limit int) ([]*apiv0.ServerResponse, string, error) + // GetServerByName retrieve latest version of a server by server name + GetServerByName(ctx context.Context, serverName string) (*apiv0.ServerResponse, error) + // GetServerByNameAndVersion retrieve specific version of a server by server name and version + GetServerByNameAndVersion(ctx context.Context, serverName string, version string) (*apiv0.ServerResponse, error) + // GetAllVersionsByServerName retrieve all versions of a server by server name + GetAllVersionsByServerName(ctx context.Context, serverName string) ([]*apiv0.ServerResponse, error) + // CreateServer creates a new server version + CreateServer(ctx context.Context, req *apiv0.ServerJSON) (*apiv0.ServerResponse, error) + // UpdateServer updates an existing server and optionally its status + UpdateServer(ctx context.Context, serverName, version string, req *apiv0.ServerJSON, newStatus *string) (*apiv0.ServerResponse, error) } diff --git a/internal/validators/validators.go b/internal/validators/validators.go index 3b448e9..0c9c319 100644 --- a/internal/validators/validators.go +++ b/internal/validators/validators.go @@ -343,7 +343,7 @@ func validateRemoteTransport(obj *model.Transport) error { } // ValidatePublishRequest validates a complete publish request including extensions -func ValidatePublishRequest(req apiv0.ServerJSON, cfg *config.Config) error { +func ValidatePublishRequest(ctx context.Context, req apiv0.ServerJSON, cfg *config.Config) error { // Validate publisher extensions in _meta if err := validatePublisherExtensions(req); err != nil { return err @@ -354,9 +354,8 @@ func ValidatePublishRequest(req apiv0.ServerJSON, cfg *config.Config) error { return err } - // Validate registry ownership for all packages if validation is enabled and server is not deleted - if cfg.EnableRegistryValidation && req.Status != model.StatusDeleted { - ctx := context.Background() + // Validate registry ownership for all packages if validation is enabled + if cfg.EnableRegistryValidation { for i, pkg := range req.Packages { if err := ValidatePackage(ctx, pkg, req.Name); err != nil { return fmt.Errorf("registry validation failed for package %d (%s): %w", i, pkg.Identifier, err) @@ -381,12 +380,8 @@ func validatePublisherExtensions(req apiv0.ServerJSON) error { } } - if req.Meta != nil { - // Validate that only publisher-provided data is allowed in _meta during publish (no official registry metadata should be present) - if req.Meta.Official != nil { - return fmt.Errorf("official registry metadata '_meta.io.modelcontextprotocol.registry/official' is not allowed during publish") - } - } + // Note: ServerJSON._meta only contains PublisherProvided data + // Official registry metadata is handled separately in the response structure return nil } diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index a2c5735..41101f1 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -1,6 +1,7 @@ package validators_test import ( + "context" "fmt" "testing" @@ -1551,7 +1552,7 @@ func TestValidate_RegistryTypesAndUrls(t *testing.T) { }, } - err := validators.ValidatePublishRequest(serverJSON, &config.Config{ + err := validators.ValidatePublishRequest(context.Background(), serverJSON, &config.Config{ EnableRegistryValidation: true, }) if tc.expectError { diff --git a/pkg/api/v0/types.go b/pkg/api/v0/types.go index 3d6589f..73d3e33 100644 --- a/pkg/api/v0/types.go +++ b/pkg/api/v0/types.go @@ -8,22 +8,31 @@ import ( // RegistryExtensions represents registry-generated metadata type RegistryExtensions struct { - ServerID string `json:"serverId"` // Consistent ID across all versions of a server - VersionID string `json:"versionId"` // Unique ID for this specific version - PublishedAt time.Time `json:"publishedAt"` - UpdatedAt time.Time `json:"updatedAt,omitempty"` - IsLatest bool `json:"isLatest"` + Status model.Status `json:"status"` + PublishedAt time.Time `json:"publishedAt"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` + IsLatest bool `json:"isLatest"` +} + +// ResponseMeta represents the top-level metadata in API responses +type ResponseMeta struct { + Official *RegistryExtensions `json:"io.modelcontextprotocol.registry/official,omitempty"` +} + +// ServerResponse represents the new API response format with separated metadata +type ServerResponse struct { + Server ServerJSON `json:"server"` + Meta ResponseMeta `json:"_meta"` } // ServerListResponse represents the paginated server list response type ServerListResponse struct { - Servers []ServerJSON `json:"servers"` - Metadata Metadata `json:"metadata"` + Servers []ServerResponse `json:"servers"` + Metadata Metadata `json:"metadata"` } // ServerMeta represents the structured metadata with known extension fields type ServerMeta struct { - Official *RegistryExtensions `json:"io.modelcontextprotocol.registry/official,omitempty"` PublisherProvided map[string]interface{} `json:"io.modelcontextprotocol.registry/publisher-provided,omitempty"` } @@ -32,7 +41,6 @@ type ServerJSON struct { Schema string `json:"$schema,omitempty"` Name string `json:"name" minLength:"1" maxLength:"200"` Description string `json:"description" minLength:"1" maxLength:"100"` - Status model.Status `json:"status,omitempty" minLength:"1"` Repository model.Repository `json:"repository,omitempty"` Version string `json:"version"` WebsiteURL string `json:"websiteUrl,omitempty"` @@ -43,20 +51,6 @@ type ServerJSON struct { // Metadata represents pagination metadata type Metadata struct { - NextCursor string `json:"next_cursor,omitempty"` + NextCursor string `json:"nextCursor,omitempty"` Count int `json:"count"` } - -func (s *ServerJSON) GetServerID() string { - if s.Meta != nil && s.Meta.Official != nil { - return s.Meta.Official.ServerID - } - return "" -} - -func (s *ServerJSON) GetVersionID() string { - if s.Meta != nil && s.Meta.Official != nil { - return s.Meta.Official.VersionID - } - return "" -} diff --git a/scripts/test_endpoints.sh b/scripts/test_endpoints.sh index 364ccdd..35a3485 100755 --- a/scripts/test_endpoints.sh +++ b/scripts/test_endpoints.sh @@ -98,25 +98,25 @@ test_servers() { echo "Response Summary:" echo "$http_response" | jq '.servers | length' | xargs echo "Total registries:" - # Display a prettier formatted summary - fixed to use lowercase property name - echo "servers Names:" - echo "$http_response" | jq -r '.servers[].name' + # Display a prettier formatted summary + echo "Server Names:" + echo "$http_response" | jq -r '.servers[].server.name' # Show the metadata with next cursor if available echo -e "\nPagination Metadata:" echo "$http_response" | jq '.metadata' # Show more detailed output with all fields - echo -e "\nservers Details:" + echo -e "\nServer Details:" echo "$http_response" | jq '.' - - echo "servers request successful" + + echo "Servers request successful" echo "-------------------------------------" return 0 else echo "Response:" echo "$http_response" | jq '.' 2>/dev/null || echo "$http_response" - echo "servers request failed" + echo "Servers request failed" echo "-------------------------------------" return 1 fi @@ -135,37 +135,37 @@ test_servers_with_limit() { if [[ $status_code == 2* ]]; then # Verify the response contains the right number of items (or not more than the limit) - item_count=$(echo "$http_response" | jq '.data | length') + item_count=$(echo "$http_response" | jq '.servers | length') echo "Response has $item_count items (requested limit: $limit)" - + # Verify we're not exceeding the limit if [[ $item_count -gt $limit ]]; then echo "ERROR: Response contains more items ($item_count) than the requested limit ($limit)" return 1 fi - + # Parse and display JSON with jq echo "Response Summary:" - + # Display a prettier formatted summary - echo "servers Names:" - echo "$http_response" | jq -r '.data[].name' + echo "Server Names:" + echo "$http_response" | jq -r '.servers[].server.name' # Show the metadata with next cursor if available echo -e "\nPagination Metadata:" echo "$http_response" | jq '.metadata' # Show more detailed output with all fields - echo -e "\nservers Details:" + echo -e "\nServer Details:" echo "$http_response" | jq '.' - - echo "servers request with limit successful" + + echo "Servers request with limit successful" echo "-------------------------------------" return 0 else echo "Response:" echo "$http_response" | jq '.' 2>/dev/null || echo "$http_response" - echo "servers request with limit failed" + echo "Servers request with limit failed" echo "-------------------------------------" return 1 fi diff --git a/scripts/test_publish.sh b/scripts/test_publish.sh index cb89559..c8ccf51 100755 --- a/scripts/test_publish.sh +++ b/scripts/test_publish.sh @@ -169,9 +169,9 @@ if [[ "${status_code:0:1}" == "2" ]]; then echo "Response:" echo "$http_response" | jq '.' 2>/dev/null || echo "$http_response" - # Check for server added message and extract UUID + # Check for server added message and extract server name message=$(echo "$http_response" | jq -r '.message // empty' 2>/dev/null) - server_id=$(echo "$http_response" | jq -r '.id // .server_id // empty' 2>/dev/null) + server_name=$(echo "$http_response" | jq -r '.server.name // .name // empty' 2>/dev/null) # Validate the response contains success indicators success_indicators=0 @@ -184,28 +184,28 @@ if [[ "${status_code:0:1}" == "2" ]]; then fi fi - if [[ ! -z "$server_id" && "$server_id" != "null" && "$server_id" != "empty" ]]; then - echo "✓ Server UUID received: $server_id" - # Validate UUID format (basic check for UUID pattern) - if [[ "$server_id" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then + if [[ ! -z "$server_name" && "$server_name" != "null" && "$server_name" != "empty" ]]; then + echo "✓ Server name received: $server_name" + # Validate server name format (should contain a namespace like 'domain/name') + if [[ "$server_name" == *"/"* ]]; then ((success_indicators++)) - echo "✓ Server ID appears to be a valid UUID format" + echo "✓ Server name appears to be a valid namespaced format" else - echo "⚠ Server ID format may not be a standard UUID: $server_id" - ((success_indicators++)) # Still count as success if we got an ID + echo "⚠ Server name format may not contain namespace: $server_name" + ((success_indicators++)) # Still count as success if we got a name fi fi if [[ $success_indicators -ge 2 ]]; then echo "" echo "🎉 PUBLISH TEST PASSED!" - echo " ✓ Server successfully published with ID: $server_id" + echo " ✓ Server successfully published with name: $server_name" echo " ✓ Success message: $message" else echo "" echo "❌ PUBLISH TEST FAILED!" - echo " Expected: Success message about server being added AND a server UUID" - echo " Received: message='$message', id='$server_id'" + echo " Expected: Success message about server being added AND a server name" + echo " Received: message='$message', name='$server_name'" exit 1 fi diff --git a/tests/integration/README.md b/tests/integration/README.md index ea77626..7e00508 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -30,3 +30,5 @@ This directory contains an end-to-end test for publishing to the registry. ```sh ./tests/integration/run.sh ``` + +**Note**: Integration tests use isolated container names (`registry-integration-test`, `postgres-integration-test`) to prevent conflicts with development containers from `make dev-compose`. diff --git a/tests/integration/docker-compose.integration-test.yml b/tests/integration/docker-compose.integration-test.yml index f5d05ec..6aa47e7 100644 --- a/tests/integration/docker-compose.integration-test.yml +++ b/tests/integration/docker-compose.integration-test.yml @@ -1,5 +1,6 @@ services: registry: + container_name: registry-integration-test environment: - MCP_REGISTRY_SEED_FROM= - MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=false @@ -9,6 +10,7 @@ services: retries: 60 timeout: 1s postgres: + container_name: postgres-integration-test volumes: !reset [] tmpfs: - /var/lib/postgresql/data:size=50m diff --git a/tests/integration/main.go b/tests/integration/main.go index 6395757..d6e8a11 100644 --- a/tests/integration/main.go +++ b/tests/integration/main.go @@ -9,6 +9,7 @@ import ( "io" "log" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -131,14 +132,14 @@ func publishToRegistry(expected *apiv0.ServerJSON, line int) error { } defer os.Remove(p) - id, err := runPublisher(p) + serverName, err := runPublisher(p) if err != nil { - log.Printf(" ⛔ Failed to get server ID: %v", err) + log.Printf(" ⛔ Failed to get server name: %v", err) return err } - log.Printf(" 📋 Got server ID: %s", id) + log.Printf(" 📋 Got server name: %s", serverName) - return verifyPublishedServer(id, expected) + return verifyPublishedServer(serverName, expected) } func runPublisher(filePath string) (string, error) { @@ -223,10 +224,10 @@ func findServerIDByName(serverName string) (string, error) { // Find the server with matching name var foundServers []string for _, server := range serverList.Servers { - if server.Name == serverName { - foundServers = append(foundServers, fmt.Sprintf("ServerID:%s VersionID:%s IsLatest:%t", server.Meta.Official.ServerID, server.Meta.Official.VersionID, server.Meta.Official.IsLatest)) + if server.Server.Name == serverName { + foundServers = append(foundServers, fmt.Sprintf("Name:%s Version:%s IsLatest:%t", server.Server.Name, server.Server.Version, server.Meta.Official.IsLatest)) if server.Meta.Official.IsLatest { - return server.Meta.Official.ServerID, nil + return server.Server.Name, nil // Return server name instead of ServerID } } } @@ -237,11 +238,14 @@ func findServerIDByName(serverName string) (string, error) { return "", fmt.Errorf("could not find any server with name %s", serverName) } -func verifyPublishedServer(id string, expected *apiv0.ServerJSON) error { - log.Printf(" 🔍 Verifying server with ID: %s", id) +func verifyPublishedServer(serverName string, expected *apiv0.ServerJSON) error { + log.Printf(" 🔍 Verifying server with name: %s", serverName) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, registryURL+"/v0/servers/"+id, nil) + + // URL encode the server name since it contains special characters like "/" + encodedName := url.PathEscape(serverName) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, registryURL+"/v0/servers/"+encodedName, nil) if err != nil { return err } @@ -258,10 +262,11 @@ func verifyPublishedServer(id string, expected *apiv0.ServerJSON) error { return fmt.Errorf("registry responded %d: %s", res.StatusCode, string(content)) } - var actual *apiv0.ServerJSON - if err := json.Unmarshal(content, &actual); err != nil { + var actualResponse *apiv0.ServerResponse + if err := json.Unmarshal(content, &actualResponse); err != nil { return fmt.Errorf("failed to unmarshal registry response: %w", err) } + actual := &actualResponse.Server if err := compareServerJSON(expected, actual); err != nil { return fmt.Errorf(`example "%s": %w`, expected.Name, err) @@ -312,9 +317,7 @@ func compareServerJSON(expected, actual *apiv0.ServerJSON) error { if expected.Description != actual.Description { return fmt.Errorf("description mismatch: expected %q, got %q", expected.Description, actual.Description) } - if expected.Status != actual.Status { - return fmt.Errorf("status mismatch: expected %q, got %q", expected.Status, actual.Status) - } + // Status comparison removed - status is now in metadata and set by registry if expected.Version != actual.Version { return fmt.Errorf("version mismatch: expected %+v, got %+v", expected.Version, actual.Version) } diff --git a/tests/integration/run.sh b/tests/integration/run.sh index 5a0718b..af7e6c2 100755 --- a/tests/integration/run.sh +++ b/tests/integration/run.sh @@ -2,11 +2,24 @@ set -e +# Use a unique project name to avoid conflicts with dev environment +export COMPOSE_PROJECT_NAME=registry-integration-test + cleanup() { echo "========== registry logs ==========" - docker logs registry || true + docker logs registry-integration-test || true echo "=========== cleaning up ===========" - docker compose down -v + + # Clean up integration test containers and volumes + docker compose -f docker-compose.yml -f tests/integration/docker-compose.integration-test.yml down -v || true + + # Force remove containers by name as backup (in case compose context is confused) + docker rm -f registry-integration-test postgres-integration-test 2>/dev/null || true + + # Clean up any leftover volumes that might have been created + docker volume prune -f || true + + echo "Cleanup completed" } go build -o ./bin/publisher ./cmd/publisher @@ -15,6 +28,10 @@ docker build -t registry . trap cleanup EXIT +# Clean up any leftover containers from previous runs +echo "Pre-cleanup: removing any leftover containers..." +cleanup + docker compose -f docker-compose.yml -f tests/integration/docker-compose.integration-test.yml up --wait --wait-timeout 60 go run tests/integration/main.go diff --git a/tools/admin/takedown.sh b/tools/admin/takedown.sh index 16d3b41..d53babf 100755 --- a/tools/admin/takedown.sh +++ b/tools/admin/takedown.sh @@ -3,15 +3,28 @@ REGISTRY_URL="${REGISTRY_URL:-https://registry.modelcontextprotocol.io}" -if [ -z "$SERVER_ID" ] || [ -z "$REGISTRY_TOKEN" ]; then - echo "Usage: REGISTRY_TOKEN= SERVER_ID= $0" +if [ -z "$SERVER_NAME" ] || [ -z "$REGISTRY_TOKEN" ]; then + echo "Usage: REGISTRY_TOKEN= SERVER_NAME= [VERSION=] $0" + echo "Example: REGISTRY_TOKEN=token SERVER_NAME=com.example/my-server ./takedown.sh" + echo "Example: REGISTRY_TOKEN=token SERVER_NAME=com.example/my-server VERSION=1.0.0 ./takedown.sh" exit 1 fi -# Get current server and update status to deleted -curl -s "${REGISTRY_URL}/v0/servers/${SERVER_ID}" | \ -jq '.status = "deleted" | {server: .}' | \ -curl -X PUT "${REGISTRY_URL}/v0/servers/${SERVER_ID}" \ +# URL encode the server name (replace / with %2F) +ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g') + +# Determine the endpoint based on whether VERSION is provided +if [ -n "$VERSION" ]; then + # Mark specific version as deleted + ENDPOINT="${REGISTRY_URL}/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}?status=deleted" + echo "Marking version $VERSION of server $SERVER_NAME as deleted..." +else + # Mark entire server as deleted (latest version) + ENDPOINT="${REGISTRY_URL}/v0/servers/${ENCODED_SERVER_NAME}?status=deleted" + echo "Marking server $SERVER_NAME as deleted..." +fi + +# Update server status to deleted +curl -X PUT "$ENDPOINT" \ -H "Authorization: Bearer ${REGISTRY_TOKEN}" \ - -H "Content-Type: application/json" \ - -d @- \ No newline at end of file + -H "Content-Type: application/json" \ No newline at end of file