diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9470bd4..2224575 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,5 +26,4 @@ jobs: registry/types/data/skill.schema.json \ registry/types/data/upstream-registry.schema.json \ registry/types/data/publisher-provided.schema.json \ - registry/types/data/toolhive-legacy-registry.schema.json \ --clobber \ No newline at end of file diff --git a/registry/types/data/toolhive-legacy-registry.schema.json b/registry/types/data/toolhive-legacy-registry.schema.json deleted file mode 100644 index 141f74f..0000000 --- a/registry/types/data/toolhive-legacy-registry.schema.json +++ /dev/null @@ -1,685 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/stacklok/toolhive-core/main/registry/types/data/toolhive-legacy-registry.schema.json", - "title": "ToolHive MCP Server Registry Schema", - "description": "JSON Schema for the ToolHive MCP server registry. This schema validates the structure and content of registry.json entries for MCP servers. See docs/registry/management.md and docs/registry/heuristics.md for inclusion criteria and management processes.", - "type": "object", - "required": [ - "last_updated", - "servers", - "version" - ], - "properties": { - "last_updated": { - "type": "string", - "description": "Timestamp when the registry was last updated, in RFC3339 format", - "format": "date-time" - }, - "servers": { - "type": "object", - "description": "Collection of MCP server entries indexed by server name", - "patternProperties": { - "^[a-z0-9][a-z0-9-]+[a-z0-9]$": { - "$ref": "#/$defs/server" - } - } - }, - "remote_servers": { - "type": "object", - "description": "Collection of remote MCP server entries indexed by server name", - "patternProperties": { - "^[a-z0-9][a-z0-9-]+[a-z0-9]$": { - "$ref": "#/$defs/remote_server" - } - } - }, - "groups": { - "type": "array", - "description": "Collection of group definitions containing related MCP servers", - "items": { - "$ref": "#/$defs/group" - } - }, - "version": { - "type": "string", - "description": "Registry schema version", - "pattern": "^\\d+\\.\\d+\\.\\d+$" - } - }, - "$defs": { - "server": { - "type": "object", - "description": "MCP server entry definition", - "required": [ - "description", - "image", - "status", - "tier", - "tools", - "transport" - ], - "properties": { - "args": { - "type": "array", - "description": "Default command-line arguments passed to the MCP server container", - "items": { - "type": "string" - }, - "default": [] - }, - "custom_metadata": { - "type": "object", - "description": "Custom user-defined metadata for the MCP server, primarily for custom registries", - "additionalProperties": true - }, - "description": { - "type": "string", - "description": "Human-readable description of the server's purpose and functionality", - "minLength": 10, - "maxLength": 500 - }, - "docker_tags": { - "type": "array", - "description": "Available Docker tags for this server image", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "env_vars": { - "type": "array", - "description": "Environment variables that can be passed to the server", - "items": { - "$ref": "#/$defs/environment_variable" - } - }, - "image": { - "type": "string", - "description": "Container image reference for the MCP server", - "pattern": "^[a-z0-9]([a-z0-9._-]*[a-z0-9])?(:[0-9]+)?(/[a-z0-9]([a-z0-9._-]*[a-z0-9])?)*(:([a-zA-Z0-9][a-zA-Z0-9._-]*))?$", - "examples": [ - "mcp/fetch:latest", - "ghcr.io/github/github-mcp-server:latest", - "mcr.microsoft.com/playwright/mcp", - "example.com:5000/team/my-app:2.0" - ] - }, - "metadata": { - "description": "Additional information about the server such as popularity metrics", - "$ref": "#/$defs/metadata" - }, - "name": { - "type": "string", - "description": "Identifier for the MCP server, used when referencing the server in commands (auto-generated from the object key)" - }, - "title": { - "type": "string", - "description": "Optional human-readable display name for the server. If not provided, the name field is used for display." - }, - "overview": { - "type": "string", - "description": "Longer Markdown-formatted description for web display. Unlike the description field (limited to 500 chars), this supports full Markdown and is intended for rich rendering on catalog pages." - }, - "permissions": { - "description": "Security profile and access permissions for the server", - "$ref": "#/$defs/permissions" - }, - "provenance": { - "description": "Verification and signing metadata", - "$ref": "#/$defs/provenance" - }, - "repository_url": { - "type": "string", - "description": "URL of the source code repository for the server", - "format": "uri" - }, - "status": { - "type": "string", - "description": "Current status of the server (Active or Deprecated)", - "enum": [ - "Active", - "Deprecated" - ] - }, - "tags": { - "type": "array", - "description": "Categorization tags for search and filtering", - "items": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9_-]*[a-z0-9]$" - }, - "minItems": 1, - "uniqueItems": true - }, - "target_port": { - "type": "integer", - "description": "Port for the container to expose (applicable to SSE and Streamable HTTP transports)", - "minimum": 1, - "maximum": 65535 - }, - "proxy_port": { - "type": "integer", - "description": "Port for the HTTP proxy to listen on (host port). If not specified, a random available port will be assigned", - "minimum": 1, - "maximum": 65535 - }, - "tier": { - "type": "string", - "description": "Tier classification of the server, (Official or Community)", - "enum": [ - "Official", - "Community" - ] - }, - "tools": { - "type": "array", - "description": "List of tool names provided by this MCP server", - "items": { - "type": "string", - "pattern": "^[\\w-]+$" - }, - "minItems": 1, - "uniqueItems": true - }, - "tool_definitions": { - "type": "array", - "description": "Full MCP Tool definitions describing the tools available from this server, including name, description, inputSchema, and annotations", - "items": { - "type": "object" - } - }, - "transport": { - "type": "string", - "description": "Communication transport protocol used by the MCP server", - "enum": [ - "stdio", - "sse", - "streamable-http" - ], - "default": "stdio" - } - } - }, - "environment_variable": { - "type": "object", - "description": "Environment variable definition for MCP server configuration", - "required": [ - "name", - "description", - "required" - ], - "properties": { - "name": { - "type": "string", - "description": "Environment variable name (e.g., API_KEY)", - "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" - }, - "description": { - "type": "string", - "description": "Human-readable explanation of the variable's purpose", - "minLength": 5, - "maxLength": 200 - }, - "required": { - "type": "boolean", - "description": "Whether this environment variable is required for the server to function", - "default": false - }, - "secret": { - "type": "boolean", - "description": "Whether this environment variable contains sensitive information that should be stored as a secret", - "default": false - }, - "default": { - "type": "string", - "description": "Value to use if the environment variable is not explicitly provided (only used for non-required variables)" - } - } - }, - "permissions": { - "type": "object", - "description": "Security permissions applied to the MCP server", - "required": [], - "properties": { - "network": { - "$ref": "#/$defs/network_permissions" - }, - "read": { - "type": "array", - "description": "File system paths the server needs read access to (will be mounted from the host)", - "items": { - "type": "string", - "pattern": "^(/[^/\\0]+)+/?$" - }, - "uniqueItems": true, - "default": [] - }, - "write": { - "type": "array", - "description": "File system paths the server needs write access to (will be mounted from the host)", - "items": { - "type": "string", - "pattern": "^(/[^/\\0]+)+/?$" - }, - "uniqueItems": true, - "default": [] - }, - "privileged": { - "type": "boolean", - "description": "Whether the container should run in privileged mode. When true, the container has access to all host devices and capabilities. Use with extreme caution as this removes most security isolation.", - "default": false - } - } - }, - "network_permissions": { - "type": "object", - "description": "Network access permissions for the MCP server", - "required": [], - "properties": { - "outbound": { - "$ref": "#/$defs/outbound_permissions" - } - } - }, - "outbound_permissions": { - "type": "object", - "description": "Outbound network access permissions", - "required": [], - "properties": { - "allow_host": { - "type": "array", - "description": "Allowed hostnames or domain patterns for outbound connections", - "items": { - "type": "string", - "anyOf": [ - { - "format": "hostname" - }, - { - "pattern": "^\\.[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$" - } - ] - }, - "uniqueItems": true, - "default": [] - }, - "allow_port": { - "type": "array", - "description": "Allowed port numbers for outbound connections", - "items": { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "uniqueItems": true, - "default": [] - }, - "insecure_allow_all": { - "type": "boolean", - "description": "Whether to allow all outbound connections (insecure, use with caution)", - "default": false - } - } - }, - "metadata": { - "type": "object", - "description": "Metadata about the MCP server from external sources", - "properties": { - "last_updated": { - "type": "string", - "description": "Timestamp when the metadata was last updated, in RFC3339 format", - "format": "date-time" - }, - "pulls": { - "type": "integer", - "description": "Number of container image pulls", - "minimum": 0 - }, - "stars": { - "type": "integer", - "description": "Number of repository stars", - "minimum": 0 - } - } - }, - "provenance": { - "type": "object", - "description": "Software supply chain provenance information for verified servers", - "properties": { - "cert_issuer": { - "type": "string", - "description": "Certificate issuer for provenance verification", - "format": "uri", - "examples": [ - "https://token.actions.githubusercontent.com" - ] - }, - "repository_uri": { - "type": "string", - "description": "Repository URI used for provenance verification", - "format": "uri" - }, - "repository_ref": { - "type": "string", - "description": "Repository reference used for provenance verification" - }, - "runner_environment": { - "type": "string", - "description": "Build environment where the server was built", - "examples": [ - "github-hosted", - "gitlab-hosted", - "self-hosted" - ] - }, - "signer_identity": { - "type": "string", - "description": "Identity of the signer for provenance verification" - }, - "sigstore_url": { - "type": "string", - "description": "Sigstore TUF repository host for provenance verification", - "format": "hostname", - "default": "tuf-repo-cdn.sigstore.dev", - "examples": [ - "tuf-repo.github.com", - "tuf-repo-cdn.sigstore.dev" - ] - }, - "attestation": { - "description": "Verified attestation information", - "$ref": "#/$defs/verified_attestation" - } - } - }, - "verified_attestation": { - "type": "object", - "description": "Verified attestation information", - "properties": { - "predicate_type": { - "type": "string", - "description": "Type of the attestation predicate", - "format": "uri", - "examples": [ - "https://slsa.dev/provenance/v0.2", - "https://slsa.dev/provenance/v1" - ] - }, - "predicate": { - "description": "Attestation predicate data" - } - } - }, - "header": { - "type": "object", - "description": "HTTP header definition for remote MCP server authentication", - "required": [ - "name", - "description", - "required" - ], - "properties": { - "name": { - "type": "string", - "description": "Header name (e.g., X-API-Key, Authorization)", - "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$" - }, - "description": { - "type": "string", - "description": "Human-readable explanation of the header's purpose", - "minLength": 5, - "maxLength": 200 - }, - "required": { - "type": "boolean", - "description": "Whether this header is required for the server to function", - "default": false - }, - "secret": { - "type": "boolean", - "description": "Whether this header contains sensitive information that should be stored as a secret", - "default": false - }, - "default": { - "type": "string", - "description": "Value to use if the header is not explicitly provided (only used for non-required headers)" - }, - "choices": { - "type": "array", - "description": "List of valid values for the header", - "items": { - "type": "string" - }, - "uniqueItems": true - } - } - }, - "oauth_config": { - "type": "object", - "description": "OAuth/OIDC configuration for remote server authentication", - "properties": { - "issuer": { - "type": "string", - "description": "OAuth/OIDC issuer URL for OIDC discovery", - "format": "uri" - }, - "authorize_url": { - "type": "string", - "description": "OAuth authorization endpoint URL (for non-OIDC OAuth)", - "format": "uri" - }, - "token_url": { - "type": "string", - "description": "OAuth token endpoint URL (for non-OIDC OAuth)", - "format": "uri" - }, - "client_id": { - "type": "string", - "description": "OAuth client ID for authentication" - }, - "scopes": { - "type": "array", - "description": "OAuth scopes to request", - "items": { - "type": "string" - } - }, - "use_pkce": { - "type": "boolean", - "description": "Whether to use PKCE for the OAuth flow", - "default": true - }, - "oauth_params": { - "type": "object", - "description": "Additional OAuth parameters to include in the authorization request (server-specific parameters like 'prompt', 'response_mode', etc.)", - "additionalProperties": { - "type": "string" - } - }, - "callback_port": { - "type": "integer", - "description": "Specific port to use for the OAuth callback server", - "minimum": 1, - "maximum": 65535 - }, - "resource": { - "type": "string", - "description": "OAuth 2.0 resource indicator (RFC 8707)" - } - } - }, - "remote_server": { - "type": "object", - "description": "Remote MCP server entry definition accessed via HTTP/HTTPS", - "required": [ - "url", - "description", - "status", - "tier", - "tools", - "transport" - ], - "properties": { - "name": { - "type": "string", - "description": "Identifier for the remote MCP server (auto-generated from the object key)" - }, - "title": { - "type": "string", - "description": "Optional human-readable display name for the server. If not provided, the name field is used for display." - }, - "overview": { - "type": "string", - "description": "Longer Markdown-formatted description for web display. Unlike the description field (limited to 500 chars), this supports full Markdown and is intended for rich rendering on catalog pages." - }, - "url": { - "type": "string", - "description": "Endpoint URL for the remote MCP server", - "format": "uri", - "examples": [ - "https://api.example.com/mcp", - "https://mcp-server.example.com/sse", - "http://localhost:8080/stream" - ] - }, - "description": { - "type": "string", - "description": "Human-readable description of the server's purpose and functionality", - "minLength": 10, - "maxLength": 500 - }, - "tier": { - "type": "string", - "description": "Tier classification of the server (Official or Community)", - "enum": [ - "Official", - "Community" - ] - }, - "status": { - "type": "string", - "description": "Current status of the server (Active or Deprecated)", - "enum": [ - "Active", - "Deprecated" - ] - }, - "transport": { - "type": "string", - "description": "Communication transport protocol used by the remote MCP server", - "enum": [ - "sse", - "streamable-http" - ], - "default": "sse" - }, - "tools": { - "type": "array", - "description": "List of tool names provided by this MCP server", - "items": { - "type": "string", - "pattern": "^[\\w-]+$" - }, - "minItems": 1, - "uniqueItems": true - }, - "tool_definitions": { - "type": "array", - "description": "Full MCP Tool definitions describing the tools available from this server, including name, description, inputSchema, and annotations", - "items": { - "type": "object" - } - }, - "headers": { - "type": "array", - "description": "HTTP headers for authentication to the remote server", - "items": { - "$ref": "#/$defs/header" - } - }, - "oauth_config": { - "description": "OAuth/OIDC configuration for authentication", - "$ref": "#/$defs/oauth_config" - }, - "env_vars": { - "type": "array", - "description": "Environment variables for client-side configuration", - "items": { - "$ref": "#/$defs/environment_variable" - } - }, - "proxy_port": { - "type": "integer", - "description": "Port for the HTTP proxy to listen on (host port). If not specified, a random available port will be assigned", - "minimum": 1, - "maximum": 65535 - }, - "metadata": { - "description": "Additional information about the server", - "$ref": "#/$defs/metadata" - }, - "repository_url": { - "type": "string", - "description": "URL of the source code repository for the server", - "format": "uri" - }, - "tags": { - "type": "array", - "description": "Categorization tags for search and filtering", - "items": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9_-]*[a-z0-9]$" - }, - "minItems": 1, - "uniqueItems": true - }, - "custom_metadata": { - "type": "object", - "description": "Custom user-defined metadata for the remote MCP server", - "additionalProperties": true - } - } - }, - "group": { - "type": "object", - "description": "Group definition containing related MCP servers that can be deployed together", - "required": [ - "name", - "description" - ], - "properties": { - "name": { - "type": "string", - "description": "Identifier for the group, used when referencing the group in commands", - "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$", - "minLength": 1, - "maxLength": 100 - }, - "description": { - "type": "string", - "description": "Human-readable description of the group's purpose and functionality", - "minLength": 10, - "maxLength": 500 - }, - "servers": { - "type": "object", - "description": "Collection of MCP server entries within this group indexed by server name", - "patternProperties": { - "^[a-z0-9][a-z0-9-]+[a-z0-9]$": { - "$ref": "#/$defs/server" - } - } - }, - "remote_servers": { - "type": "object", - "description": "Collection of remote MCP server entries within this group indexed by server name", - "patternProperties": { - "^[a-z0-9][a-z0-9-]+[a-z0-9]$": { - "$ref": "#/$defs/remote_server" - } - } - } - } - } - } -} diff --git a/registry/types/registry_types.go b/registry/types/registry_types.go index 61c4470..1ffb6c6 100644 --- a/registry/types/registry_types.go +++ b/registry/types/registry_types.go @@ -14,13 +14,6 @@ import ( "github.com/stacklok/toolhive-core/permissions" ) -// Updates to the registry schema should be reflected in the JSON schema file located at -// registry/types/data/toolhive-legacy-registry.schema.json. -// The schema is used for validation and documentation purposes. -// -// The embedded registry.json is automatically validated against the schema during tests. -// See registry/types/schema_validation_test.go for the validation implementation. - // Group represents a collection of related MCP servers that can be deployed together type Group struct { // Name is the identifier for the group, used when referencing the group in commands diff --git a/registry/types/schema_validation.go b/registry/types/schema_validation.go index 2476952..2975717 100644 --- a/registry/types/schema_validation.go +++ b/registry/types/schema_validation.go @@ -14,7 +14,7 @@ import ( "github.com/xeipuuv/gojsonschema" ) -//go:embed data/toolhive-legacy-registry.schema.json data/upstream-registry.schema.json data/publisher-provided.schema.json data/skill.schema.json data/server.schema.json +//go:embed data/upstream-registry.schema.json data/publisher-provided.schema.json data/skill.schema.json data/server.schema.json var embeddedSchemaFS embed.FS // referencedSchemas lists embedded schema files that declare an $id matching @@ -50,15 +50,6 @@ func ensurePreloaded() error { return preloadErr } -// Validate validates the Registry against the legacy ToolHive registry schema. -func (r *Registry) Validate() error { - data, err := json.Marshal(r) - if err != nil { - return fmt.Errorf("failed to serialize registry: %w", err) - } - return validateAgainstSchema(data, "data/toolhive-legacy-registry.schema.json", "registry schema validation failed") -} - // Validate validates the UpstreamRegistry against the upstream registry schema. // It also validates any publisher-provided extensions found in server definitions. func (r *UpstreamRegistry) Validate() error { @@ -91,11 +82,6 @@ func (s *Skill) Validate() error { return validateAgainstSchema(data, "data/skill.schema.json", "skill schema validation failed") } -// ValidateRegistrySchema validates raw registry JSON bytes against the legacy ToolHive registry schema. -func ValidateRegistrySchema(registryData []byte) error { - return validateAgainstSchema(registryData, "data/toolhive-legacy-registry.schema.json", "registry schema validation failed") -} - // ValidateUpstreamRegistryBytes validates raw upstream registry JSON bytes against the upstream registry schema. // It also validates any publisher-provided extensions found in server definitions. func ValidateUpstreamRegistryBytes(registryData []byte) error { diff --git a/registry/types/schema_validation_test.go b/registry/types/schema_validation_test.go index 38c04ae..528af62 100644 --- a/registry/types/schema_validation_test.go +++ b/registry/types/schema_validation_test.go @@ -16,359 +16,6 @@ import ( "github.com/stretchr/testify/require" ) -// TestRegistrySchemaValidation tests the schema validation function with various inputs -func TestRegistrySchemaValidation(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - registryJSON string - expectError bool - errorContains string - }{ - { - name: "valid minimal registry", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": {} - }`, - expectError: false, - }, - { - name: "valid registry with server", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "description": "A test server for validation", - "image": "test/server:latest", - "status": "Active", - "tier": "Community", - "tools": ["test_tool"], - "transport": "stdio" - } - } - }`, - expectError: false, - }, - { - name: "missing required version field", - registryJSON: `{ - "last_updated": "2025-01-01T00:00:00Z", - "servers": {} - }`, - expectError: true, - errorContains: errKeyVersion, - }, - { - name: "missing required last_updated field", - registryJSON: `{ - "version": "1.0.0", - "servers": {} - }`, - expectError: true, - errorContains: errKeyLastUpdated, - }, - { - name: "missing required servers field", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z" - }`, - expectError: true, - errorContains: "servers", - }, - { - name: "invalid version format", - registryJSON: `{ - "version": "invalid-version", - "last_updated": "2025-01-01T00:00:00Z", - "servers": {} - }`, - expectError: true, - errorContains: errKeyVersion, - }, - { - name: "invalid date format", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "invalid-date", - "servers": {} - }`, - expectError: true, - errorContains: errKeyLastUpdated, - }, - { - name: "server missing required description", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "image": "test/server:latest", - "status": "Active", - "tier": "Community", - "tools": ["test_tool"], - "transport": "stdio" - } - } - }`, - expectError: true, - errorContains: errKeyDescription, - }, - { - name: "server missing required image", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "description": "A test server for validation", - "status": "Active", - "tier": "Community", - "tools": ["test_tool"], - "transport": "stdio" - } - } - }`, - expectError: true, - errorContains: "image", - }, - { - name: "server with invalid status", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "description": "A test server for validation", - "image": "test/server:latest", - "status": "InvalidStatus", - "tier": "Community", - "tools": ["test_tool"], - "transport": "stdio" - } - } - }`, - expectError: true, - errorContains: errKeyStatus, - }, - { - name: "server with invalid tier", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "description": "A test server for validation", - "image": "test/server:latest", - "status": "Active", - "tier": "InvalidTier", - "tools": ["test_tool"], - "transport": "stdio" - } - } - }`, - expectError: true, - errorContains: errKeyTier, - }, - { - name: "server with invalid transport", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "description": "A test server for validation", - "image": "test/server:latest", - "status": "Active", - "tier": "Community", - "tools": ["test_tool"], - "transport": "invalid-transport" - } - } - }`, - expectError: true, - errorContains: "transport", - }, - { - name: "server with empty tools array", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "description": "A test server for validation", - "image": "test/server:latest", - "status": "Active", - "tier": "Community", - "tools": [], - "transport": "stdio" - } - } - }`, - expectError: true, - errorContains: "tools", - }, - { - name: "server with description too short", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "description": "Short", - "image": "test/server:latest", - "status": "Active", - "tier": "Community", - "tools": ["test_tool"], - "transport": "stdio" - } - } - }`, - expectError: true, - errorContains: errKeyDescription, - }, - { - name: "non-matching server name passes without additionalProperties restriction", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "Invalid_Server_Name": { - "description": "A test server for validation", - "image": "test/server:latest", - "status": "Active", - "tier": "Community", - "tools": ["test_tool"], - "transport": "stdio" - } - } - }`, - expectError: false, - }, - { - name: "valid remote server", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": {}, - "remote_servers": { - "test-remote": { - "url": "https://api.example.com/mcp", - "description": "A test remote server for validation", - "status": "Active", - "tier": "Community", - "tools": ["remote_tool"], - "transport": "sse" - } - } - }`, - expectError: false, - }, - { - name: "remote server with invalid transport (stdio not allowed)", - registryJSON: `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": {}, - "remote_servers": { - "test-remote": { - "url": "https://api.example.com/mcp", - "description": "A test remote server for validation", - "status": "Active", - "tier": "Community", - "tools": ["remote_tool"], - "transport": "stdio" - } - } - }`, - expectError: true, - errorContains: "transport", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - err := ValidateRegistrySchema([]byte(tt.registryJSON)) - - if tt.expectError { - require.Error(t, err, "Expected validation to fail for test case: %s", tt.name) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains, "Error should contain expected text") - } - } else { - require.NoError(t, err, "Expected validation to pass for test case: %s", tt.name) - } - }) - } -} - -// TestValidateRegistrySchemaWithInvalidJSON tests that the function handles invalid JSON gracefully -func TestValidateRegistrySchemaWithInvalidJSON(t *testing.T) { - t.Parallel() - - invalidJSON := `{ - "version": "1.0.0", - "last_updated": "2025-01-01T00:00:00Z", - "servers": { - "test-server": { - "description": "A test server" - // Missing comma - invalid JSON - "image": "test/server:latest" - } - } - }` - - err := ValidateRegistrySchema([]byte(invalidJSON)) - require.Error(t, err) - // gojsonschema returns validation error for invalid JSON - assert.Contains(t, err.Error(), "invalid character") -} - -// TestMultipleValidationErrors tests that multiple validation errors are reported together -func TestMultipleValidationErrors(t *testing.T) { - t.Parallel() - - // Registry with multiple validation errors - invalidRegistryJSON := `{ - "servers": { - "test-server": { - "description": "Short", - "status": "InvalidStatus", - "tier": "InvalidTier", - "tools": [], - "transport": "invalid-transport" - } - } - }` - - err := ValidateRegistrySchema([]byte(invalidRegistryJSON)) - require.Error(t, err, "Expected validation to fail with multiple errors") - - errorMsg := err.Error() - - // Should contain multiple errors - assert.Contains(t, errorMsg, "validation failed with", "Should indicate multiple errors") - - // Should contain specific error details - assert.Contains(t, errorMsg, errKeyVersion, "Should mention missing version") - assert.Contains(t, errorMsg, errKeyLastUpdated, "Should mention missing last_updated") - assert.Contains(t, errorMsg, errKeyDescription, "Should mention description length issue") - assert.Contains(t, errorMsg, errKeyStatus, "Should mention invalid status") - assert.Contains(t, errorMsg, "tools", "Should mention empty tools array") - - // Verify it's formatted as a numbered list - assert.Contains(t, errorMsg, "1.", "Should have numbered error list") - assert.Contains(t, errorMsg, "2.", "Should have multiple numbered errors") - - t.Logf("Multi-error output:\n%s", errorMsg) -} - // TestValidateUpstreamRegistry tests the ValidateUpstreamRegistry function func TestValidateUpstreamRegistryBytes(t *testing.T) { t.Parallel() @@ -1506,44 +1153,6 @@ func TestValidateSkillBytes(t *testing.T) { } } -// TestRegistry_Validate tests the Validate method on the Registry struct. -func TestRegistry_Validate(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - registry *Registry - expectError bool - }{ - { - name: "valid minimal registry", - registry: &Registry{ - Version: testVersion, - LastUpdated: "2025-01-01T00:00:00Z", - Servers: map[string]*ImageMetadata{}, - }, - expectError: false, - }, - { - name: "invalid registry - empty struct", - registry: &Registry{}, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - err := tc.registry.Validate() - if tc.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - // TestUpstreamRegistry_Validate tests the Validate method on the UpstreamRegistry struct. func TestUpstreamRegistry_Validate(t *testing.T) { t.Parallel()