From 9d1455781a90be46a361a21f8d8fa1565a41875a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:16:48 +0000 Subject: [PATCH 1/4] Initial plan From b2429de3f991688b472666ad8e4c17e137a78222 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:26:33 +0000 Subject: [PATCH 2/4] Update MCP registry to use v0.1 API endpoint and format Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/mcp_add_test.go | 28 +++-- pkg/cli/mcp_registry.go | 38 ++++-- pkg/cli/mcp_registry_improvements_test.go | 17 ++- pkg/cli/mcp_registry_test.go | 146 ++++++++++++---------- pkg/cli/mcp_registry_types.go | 97 +++++++++----- pkg/constants/constants.go | 4 +- pkg/constants/constants_test.go | 6 +- 7 files changed, 212 insertions(+), 124 deletions(-) diff --git a/pkg/cli/mcp_add_test.go b/pkg/cli/mcp_add_test.go index f5b1b91821..a1647a1186 100644 --- a/pkg/cli/mcp_add_test.go +++ b/pkg/cli/mcp_add_test.go @@ -489,16 +489,30 @@ func TestListAvailableServers(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/servers" { response := ServerListResponse{ - Servers: []Server{ + Servers: []ServerResponse{ { - Name: "io.github.makenotion/notion-mcp-server", - Description: "Connect to Notion API", - Status: "active", + Server: ServerDetail{ + Name: "io.github.makenotion/notion-mcp-server", + Description: "Connect to Notion API", + Version: "1.0.0", + }, + Meta: map[string]any{ + "io.modelcontextprotocol.registry/official": map[string]any{ + "status": "active", + }, + }, }, { - Name: "io.github.example/github-mcp-server", - Description: "Connect to GitHub API", - Status: "active", + Server: ServerDetail{ + Name: "io.github.example/github-mcp-server", + Description: "Connect to GitHub API", + Version: "1.0.0", + }, + Meta: map[string]any{ + "io.modelcontextprotocol.registry/official": map[string]any{ + "status": "active", + }, + }, }, }, } diff --git a/pkg/cli/mcp_registry.go b/pkg/cli/mcp_registry.go index dfe8d17d8e..482dbce489 100644 --- a/pkg/cli/mcp_registry.go +++ b/pkg/cli/mcp_registry.go @@ -127,10 +127,14 @@ func (c *MCPRegistryClient) SearchServers(query string) ([]MCPRegistryServerForP // Convert servers to flattened format and filter by status mcpRegistryLog.Printf("Processing %d servers from registry", len(response.Servers)) servers := make([]MCPRegistryServerForProcessing, 0, len(response.Servers)) - for _, server := range response.Servers { - // Only include active servers - if server.Status != StatusActive { - continue + for _, serverResp := range response.Servers { + server := serverResp.Server + + // Only include active servers (check in _meta) + if meta, ok := serverResp.Meta["io.modelcontextprotocol.registry/official"].(map[string]any); ok { + if status, ok := meta["status"].(string); ok && status != StatusActive { + continue + } } processedServer := MCPRegistryServerForProcessing{ @@ -139,7 +143,7 @@ func (c *MCPRegistryClient) SearchServers(query string) ([]MCPRegistryServerForP } // Set repository URL if available - if server.Repository.URL != "" { + if server.Repository != nil && server.Repository.URL != "" { processedServer.Repository = server.Repository.URL } @@ -148,7 +152,9 @@ func (c *MCPRegistryClient) SearchServers(query string) ([]MCPRegistryServerForP pkg := server.Packages[0] // Use transport type from package - processedServer.Transport = pkg.Transport.Type + if pkg.Transport != nil { + processedServer.Transport = pkg.Transport.Type + } if processedServer.Transport == "" { processedServer.Transport = "stdio" // default fallback } @@ -312,8 +318,18 @@ func (c *MCPRegistryClient) GetServer(serverName string) (*MCPRegistryServerForP spinner.StopWithMessage(fmt.Sprintf("✓ Fetched MCP server '%s'", serverName)) // Find exact match by name, filtering locally - for _, server := range response.Servers { - if server.Name == serverName && server.Status == StatusActive { + for _, serverResp := range response.Servers { + server := serverResp.Server + + // Check status from _meta + isActive := true + if meta, ok := serverResp.Meta["io.modelcontextprotocol.registry/official"].(map[string]any); ok { + if status, ok := meta["status"].(string); ok { + isActive = (status == StatusActive) + } + } + + if server.Name == serverName && isActive { // Convert to flattened format similar to SearchServers processedServer := MCPRegistryServerForProcessing{ Name: server.Name, @@ -321,7 +337,7 @@ func (c *MCPRegistryClient) GetServer(serverName string) (*MCPRegistryServerForP } // Set repository URL if available - if server.Repository.URL != "" { + if server.Repository != nil && server.Repository.URL != "" { processedServer.Repository = server.Repository.URL } @@ -330,7 +346,9 @@ func (c *MCPRegistryClient) GetServer(serverName string) (*MCPRegistryServerForP pkg := server.Packages[0] // Use transport type from package - processedServer.Transport = pkg.Transport.Type + if pkg.Transport != nil { + processedServer.Transport = pkg.Transport.Type + } if processedServer.Transport == "" { processedServer.Transport = "stdio" // default fallback } diff --git a/pkg/cli/mcp_registry_improvements_test.go b/pkg/cli/mcp_registry_improvements_test.go index 67af075228..77b357f69a 100644 --- a/pkg/cli/mcp_registry_improvements_test.go +++ b/pkg/cli/mcp_registry_improvements_test.go @@ -110,10 +110,17 @@ func TestMCPRegistryClient_FlexibleValidation(t *testing.T) { servers := make([]string, tc.serverCount) for i := 0; i < tc.serverCount; i++ { servers[i] = `{ - "name": "test/server-` + string(rune('1'+i)) + `", - "description": "Test server", - "status": "active", - "packages": [{"identifier": "test", "transport": {"type": "stdio"}}] + "server": { + "name": "test/server-` + string(rune('1'+i)) + `", + "description": "Test server", + "version": "1.0.0", + "packages": [{"identifier": "test", "transport": {"type": "stdio"}}] + }, + "_meta": { + "io.modelcontextprotocol.registry/official": { + "status": "active" + } + } }` } @@ -131,7 +138,7 @@ func TestMCPRegistryClient_FlexibleValidation(t *testing.T) { if tc.useProduction { // For production tests, create client with production URL pattern // but we'll need to manually test the validation logic - client = NewMCPRegistryClient("https://api.mcp.github.com/v0") + client = NewMCPRegistryClient("https://api.mcp.github.com/v0.1") } else { // For custom registry tests, use test server URL client = NewMCPRegistryClient(server.URL) diff --git a/pkg/cli/mcp_registry_test.go b/pkg/cli/mcp_registry_test.go index 88d42974a6..3e6647600e 100644 --- a/pkg/cli/mcp_registry_test.go +++ b/pkg/cli/mcp_registry_test.go @@ -13,43 +13,51 @@ func TestMCPRegistryClient_SearchServers(t *testing.T) { t.Errorf("Expected path /servers, got %s", r.URL.Path) } - // Return mock response with new structure based on official specification + // Return mock response with v0.1 structure based on official specification response := `{ "servers": [ { - "name": "io.github.makenotion/notion-mcp-server", - "description": "MCP server for Notion integration", - "status": "active", - "version": "1.0.0", - "repository": { - "url": "https://github.com/example/notion-mcp", - "source": "github" + "server": { + "name": "io.github.makenotion/notion-mcp-server", + "description": "MCP server for Notion integration", + "version": "1.0.0", + "repository": { + "url": "https://github.com/example/notion-mcp", + "source": "github" + }, + "packages": [ + { + "registryType": "npm", + "identifier": "notion-mcp", + "version": "1.0.0", + "runtimeHint": "node", + "transport": { + "type": "stdio" + }, + "packageArguments": [ + { + "type": "positional", + "value": "notion-mcp" + } + ], + "environmentVariables": [ + { + "name": "NOTION_TOKEN", + "description": "Notion API token", + "isRequired": true, + "isSecret": true + } + ] + } + ] }, - "packages": [ - { - "registry_type": "npm", - "identifier": "notion-mcp", - "version": "1.0.0", - "runtime_hint": "node", - "transport": { - "type": "stdio" - }, - "package_arguments": [ - { - "type": "positional", - "value": "notion-mcp" - } - ], - "environment_variables": [ - { - "name": "NOTION_TOKEN", - "description": "Notion API token", - "is_required": true, - "is_secret": true - } - ] + "_meta": { + "io.modelcontextprotocol.registry/official": { + "status": "active", + "publishedAt": "2025-01-01T10:30:00Z", + "isLatest": true } - ] + } } ] }` @@ -101,43 +109,51 @@ func TestMCPRegistryClient_GetServer(t *testing.T) { // No longer check for search query parameter since we now fetch all servers and filter locally - // Return mock response with new structure based on official specification + // Return mock response with v0.1 structure based on official specification response := `{ "servers": [ { - "name": "io.github.makenotion/notion-mcp-server", - "description": "MCP server for Notion integration", - "status": "active", - "version": "1.0.0", - "repository": { - "url": "https://github.com/example/notion-mcp", - "source": "github" + "server": { + "name": "io.github.makenotion/notion-mcp-server", + "description": "MCP server for Notion integration", + "version": "1.0.0", + "repository": { + "url": "https://github.com/example/notion-mcp", + "source": "github" + }, + "packages": [ + { + "registryType": "npm", + "identifier": "notion-mcp", + "version": "1.0.0", + "runtimeHint": "node", + "transport": { + "type": "stdio" + }, + "packageArguments": [ + { + "type": "positional", + "value": "notion-mcp" + } + ], + "environmentVariables": [ + { + "name": "NOTION_TOKEN", + "description": "Notion API token", + "isRequired": true, + "isSecret": true + } + ] + } + ] }, - "packages": [ - { - "registry_type": "npm", - "identifier": "notion-mcp", - "version": "1.0.0", - "runtime_hint": "node", - "transport": { - "type": "stdio" - }, - "package_arguments": [ - { - "type": "positional", - "value": "notion-mcp" - } - ], - "environment_variables": [ - { - "name": "NOTION_TOKEN", - "description": "Notion API token", - "is_required": true, - "is_secret": true - } - ] + "_meta": { + "io.modelcontextprotocol.registry/official": { + "status": "active", + "publishedAt": "2025-01-01T10:30:00Z", + "isLatest": true } - ] + } } ] }` @@ -191,7 +207,7 @@ func TestMCPRegistryClient_GetServerNotFound(t *testing.T) { func TestNewMCPRegistryClient_DefaultURL(t *testing.T) { client := NewMCPRegistryClient("") - expectedURL := "https://api.mcp.github.com/v0" + expectedURL := "https://api.mcp.github.com/v0.1" if client.registryURL != expectedURL { t.Errorf("Expected default registry URL '%s', got '%s'", expectedURL, client.registryURL) } diff --git a/pkg/cli/mcp_registry_types.go b/pkg/cli/mcp_registry_types.go index 738068baac..a44c231654 100644 --- a/pkg/cli/mcp_registry_types.go +++ b/pkg/cli/mcp_registry_types.go @@ -1,74 +1,106 @@ package cli -// Local types inferred from GitHub MCP Registry API structure -// This replaces the dependency on github.com/modelcontextprotocol/registry +// Local types inferred from GitHub MCP Registry API v0.1 structure +// Based on the official specification at: +// https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/api/openapi.yaml -// ServerListResponse represents the response from the /servers endpoint +// ServerListResponse represents the response from the /v0.1/servers endpoint type ServerListResponse struct { - Servers []Server `json:"servers"` + Servers []ServerResponse `json:"servers"` + Metadata *Metadata `json:"metadata,omitempty"` } -// Server represents an MCP server in the registry -type Server struct { +// Metadata represents pagination metadata +type Metadata struct { + NextCursor string `json:"nextCursor,omitempty"` + Count int `json:"count,omitempty"` +} + +// ServerResponse represents the API response format with separated server data and registry metadata +type ServerResponse struct { + Server ServerDetail `json:"server"` + Meta map[string]any `json:"_meta,omitempty"` +} + +// ServerDetail represents an MCP server in the registry +type ServerDetail struct { Name string `json:"name"` Description string `json:"description"` - Status string `json:"status"` - Version string `json:"version,omitempty"` - Repository Repository `json:"repository,omitempty"` + Title string `json:"title,omitempty"` + Version string `json:"version"` + Repository *Repository `json:"repository,omitempty"` Packages []MCPPackage `json:"packages,omitempty"` Remotes []Remote `json:"remotes,omitempty"` + WebsiteURL string `json:"websiteUrl,omitempty"` + Schema string `json:"$schema,omitempty"` + InternalMeta map[string]any `json:"_meta,omitempty"` } // Repository represents the source repository information type Repository struct { - URL string `json:"url"` - Source string `json:"source,omitempty"` + URL string `json:"url"` + Source string `json:"source,omitempty"` + ID string `json:"id,omitempty"` + Subfolder string `json:"subfolder,omitempty"` } // MCPPackage represents a package configuration for an MCP server type MCPPackage struct { - RegistryType string `json:"registry_type,omitempty"` + RegistryType string `json:"registryType,omitempty"` + RegistryBaseURL string `json:"registryBaseUrl,omitempty"` Identifier string `json:"identifier,omitempty"` Version string `json:"version,omitempty"` - RuntimeHint string `json:"runtime_hint,omitempty"` - Transport Transport `json:"transport,omitempty"` - RuntimeArguments []Argument `json:"runtime_arguments,omitempty"` - PackageArguments []Argument `json:"package_arguments,omitempty"` - EnvironmentVariables []EnvironmentVariable `json:"environment_variables,omitempty"` + FileSHA256 string `json:"fileSha256,omitempty"` + RuntimeHint string `json:"runtimeHint,omitempty"` + Transport *Transport `json:"transport,omitempty"` + RuntimeArguments []Argument `json:"runtimeArguments,omitempty"` + PackageArguments []Argument `json:"packageArguments,omitempty"` + EnvironmentVariables []EnvironmentVariable `json:"environmentVariables,omitempty"` } // Transport represents the transport configuration type Transport struct { - Type string `json:"type"` + Type string `json:"type"` + URL string `json:"url,omitempty"` + Headers []EnvironmentVariable `json:"headers,omitempty"` + Variables map[string]any `json:"variables,omitempty"` } // Argument represents a command line argument type Argument struct { - Type string `json:"type"` - Value string `json:"value,omitempty"` + Type string `json:"type"` + Value string `json:"value,omitempty"` + Name string `json:"name,omitempty"` // For named arguments + ValueHint string `json:"valueHint,omitempty"` // For positional arguments + IsRepeated bool `json:"isRepeated,omitempty"` + Description string `json:"description,omitempty"` + IsRequired bool `json:"isRequired,omitempty"` + Format string `json:"format,omitempty"` + IsSecret bool `json:"isSecret,omitempty"` + Default string `json:"default,omitempty"` + Placeholder string `json:"placeholder,omitempty"` + Choices []string `json:"choices,omitempty"` + Variables map[string]any `json:"variables,omitempty"` } // EnvironmentVariable represents an environment variable configuration type EnvironmentVariable struct { Name string `json:"name"` Description string `json:"description,omitempty"` - IsRequired bool `json:"is_required,omitempty"` - IsSecret bool `json:"is_secret,omitempty"` + IsRequired bool `json:"isRequired,omitempty"` + IsSecret bool `json:"isSecret,omitempty"` Default string `json:"default,omitempty"` + Format string `json:"format,omitempty"` + Placeholder string `json:"placeholder,omitempty"` + Choices []string `json:"choices,omitempty"` } // Remote represents a remote server configuration type Remote struct { - Type string `json:"type"` - URL string `json:"url"` - Headers []Header `json:"headers,omitempty"` -} - -// Header represents an HTTP header for remote servers -type Header struct { - Name string `json:"name"` - IsSecret bool `json:"is_secret,omitempty"` - Default string `json:"default,omitempty"` + Type string `json:"type"` + URL string `json:"url"` + Headers []EnvironmentVariable `json:"headers,omitempty"` + Variables map[string]any `json:"variables,omitempty"` } // Status constants for server status @@ -80,4 +112,5 @@ const ( // Argument type constants const ( ArgumentTypePositional = "positional" + ArgumentTypeNamed = "named" ) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 34eb4271fd..7208101c73 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -91,7 +91,7 @@ func (f FeatureFlag) IsValid() bool { // // Example usage: // -// const DefaultMCPRegistryURL URL = "https://api.mcp.github.com/v0" +// const DefaultMCPRegistryURL URL = "https://api.mcp.github.com/v0.1" // func FetchFromRegistry(url URL) error { ... } type URL string @@ -232,7 +232,7 @@ const MaxExpressionLineLength LineLength = 120 const ExpressionBreakThreshold LineLength = 100 // DefaultMCPRegistryURL is the default MCP registry URL. -const DefaultMCPRegistryURL URL = "https://api.mcp.github.com/v0" +const DefaultMCPRegistryURL URL = "https://api.mcp.github.com/v0.1" // GitHubCopilotMCPDomain is the domain for the hosted GitHub MCP server. // Used when github tool is configured with mode: remote. diff --git a/pkg/constants/constants_test.go b/pkg/constants/constants_test.go index 0bc89c8b58..9f00de309f 100644 --- a/pkg/constants/constants_test.go +++ b/pkg/constants/constants_test.go @@ -224,7 +224,7 @@ func TestConstantValues(t *testing.T) { expected string }{ {"CLIExtensionPrefix", string(CLIExtensionPrefix), "gh aw"}, - {"DefaultMCPRegistryURL", string(DefaultMCPRegistryURL), "https://api.mcp.github.com/v0"}, + {"DefaultMCPRegistryURL", string(DefaultMCPRegistryURL), "https://api.mcp.github.com/v0.1"}, {"AgentJobName", string(AgentJobName), "agent"}, {"ActivationJobName", string(ActivationJobName), "activation"}, {"PreActivationJobName", string(PreActivationJobName), "pre_activation"}, @@ -430,8 +430,8 @@ func TestSemanticTypeAliases(t *testing.T) { // Test DefaultMCPRegistryURL has the correct type registryURL := DefaultMCPRegistryURL - if string(registryURL) != "https://api.mcp.github.com/v0" { - t.Errorf("DefaultMCPRegistryURL = %q, want %q", registryURL, "https://api.mcp.github.com/v0") + if string(registryURL) != "https://api.mcp.github.com/v0.1" { + t.Errorf("DefaultMCPRegistryURL = %q, want %q", registryURL, "https://api.mcp.github.com/v0.1") } }) From 588dc725897fe45cd1c2806909df1358fab2fd3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:38:16 +0000 Subject: [PATCH 3/4] Fix help text to show v0.1 registry URL Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/mcp_add.go | 2 +- pkg/cli/mcp_registry.go | 6 ++--- pkg/cli/mcp_registry_types.go | 42 +++++++++++++++++------------------ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pkg/cli/mcp_add.go b/pkg/cli/mcp_add.go index d65a98ce7e..53f1a52d0d 100644 --- a/pkg/cli/mcp_add.go +++ b/pkg/cli/mcp_add.go @@ -329,7 +329,7 @@ The command will: - Add the MCP tool configuration to the workflow's frontmatter - Automatically compile the workflow to generate the .lock.yml file -Registry URL defaults to: https://api.mcp.github.com/v0`, +Registry URL defaults to: https://api.mcp.github.com/v0.1`, Args: cobra.RangeArgs(0, 2), RunE: func(cmd *cobra.Command, args []string) error { verbose, _ := cmd.Flags().GetBool("verbose") diff --git a/pkg/cli/mcp_registry.go b/pkg/cli/mcp_registry.go index 482dbce489..a31c2de35e 100644 --- a/pkg/cli/mcp_registry.go +++ b/pkg/cli/mcp_registry.go @@ -129,7 +129,7 @@ func (c *MCPRegistryClient) SearchServers(query string) ([]MCPRegistryServerForP servers := make([]MCPRegistryServerForProcessing, 0, len(response.Servers)) for _, serverResp := range response.Servers { server := serverResp.Server - + // Only include active servers (check in _meta) if meta, ok := serverResp.Meta["io.modelcontextprotocol.registry/official"].(map[string]any); ok { if status, ok := meta["status"].(string); ok && status != StatusActive { @@ -320,7 +320,7 @@ func (c *MCPRegistryClient) GetServer(serverName string) (*MCPRegistryServerForP // Find exact match by name, filtering locally for _, serverResp := range response.Servers { server := serverResp.Server - + // Check status from _meta isActive := true if meta, ok := serverResp.Meta["io.modelcontextprotocol.registry/official"].(map[string]any); ok { @@ -328,7 +328,7 @@ func (c *MCPRegistryClient) GetServer(serverName string) (*MCPRegistryServerForP isActive = (status == StatusActive) } } - + if server.Name == serverName && isActive { // Convert to flattened format similar to SearchServers processedServer := MCPRegistryServerForProcessing{ diff --git a/pkg/cli/mcp_registry_types.go b/pkg/cli/mcp_registry_types.go index a44c231654..bd4056111c 100644 --- a/pkg/cli/mcp_registry_types.go +++ b/pkg/cli/mcp_registry_types.go @@ -18,21 +18,21 @@ type Metadata struct { // ServerResponse represents the API response format with separated server data and registry metadata type ServerResponse struct { - Server ServerDetail `json:"server"` + Server ServerDetail `json:"server"` Meta map[string]any `json:"_meta,omitempty"` } // ServerDetail represents an MCP server in the registry type ServerDetail struct { - Name string `json:"name"` - Description string `json:"description"` - Title string `json:"title,omitempty"` - Version string `json:"version"` - Repository *Repository `json:"repository,omitempty"` - Packages []MCPPackage `json:"packages,omitempty"` - Remotes []Remote `json:"remotes,omitempty"` - WebsiteURL string `json:"websiteUrl,omitempty"` - Schema string `json:"$schema,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Title string `json:"title,omitempty"` + Version string `json:"version"` + Repository *Repository `json:"repository,omitempty"` + Packages []MCPPackage `json:"packages,omitempty"` + Remotes []Remote `json:"remotes,omitempty"` + WebsiteURL string `json:"websiteUrl,omitempty"` + Schema string `json:"$schema,omitempty"` InternalMeta map[string]any `json:"_meta,omitempty"` } @@ -60,17 +60,17 @@ type MCPPackage struct { // Transport represents the transport configuration type Transport struct { - Type string `json:"type"` - URL string `json:"url,omitempty"` + Type string `json:"type"` + URL string `json:"url,omitempty"` Headers []EnvironmentVariable `json:"headers,omitempty"` - Variables map[string]any `json:"variables,omitempty"` + Variables map[string]any `json:"variables,omitempty"` } // Argument represents a command line argument type Argument struct { Type string `json:"type"` Value string `json:"value,omitempty"` - Name string `json:"name,omitempty"` // For named arguments + Name string `json:"name,omitempty"` // For named arguments ValueHint string `json:"valueHint,omitempty"` // For positional arguments IsRepeated bool `json:"isRepeated,omitempty"` Description string `json:"description,omitempty"` @@ -85,13 +85,13 @@ type Argument struct { // EnvironmentVariable represents an environment variable configuration type EnvironmentVariable struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - IsRequired bool `json:"isRequired,omitempty"` - IsSecret bool `json:"isSecret,omitempty"` - Default string `json:"default,omitempty"` - Format string `json:"format,omitempty"` - Placeholder string `json:"placeholder,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + IsRequired bool `json:"isRequired,omitempty"` + IsSecret bool `json:"isSecret,omitempty"` + Default string `json:"default,omitempty"` + Format string `json:"format,omitempty"` + Placeholder string `json:"placeholder,omitempty"` Choices []string `json:"choices,omitempty"` } From 6b3982b6ed0e49ee6fa58f433bf95f643c83c3d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:48:47 +0000 Subject: [PATCH 4/4] Add comprehensive live integration tests for MCP registry v0.1 API - Add TestMCPRegistryClient_LiveSearchServers with subtests: - search_all_servers: validates fetching and parsing all servers - search_with_query: tests filtering servers by query string - verify_transport_types: checks support for different transport types - Add TestMCPRegistryClient_LiveGetServer with subtests: - get_github_server: retrieves and validates actual server from registry - get_nonexistent_server: verifies error handling for missing servers - Add TestMCPRegistryClient_LiveResponseStructure with subtests: - validate_v0.1_fields: confirms parsing of packageArguments, environmentVariables, runtimeHint, repository - validate_transport_parsing: validates transport type extraction All tests skip gracefully on network restrictions and pass with live API Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/mcp_registry_live_test.go | 286 ++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 pkg/cli/mcp_registry_live_test.go diff --git a/pkg/cli/mcp_registry_live_test.go b/pkg/cli/mcp_registry_live_test.go new file mode 100644 index 0000000000..047cbced19 --- /dev/null +++ b/pkg/cli/mcp_registry_live_test.go @@ -0,0 +1,286 @@ +package cli + +import ( + "strings" + "testing" + + "github.com/githubnext/gh-aw/pkg/constants" +) + +// TestMCPRegistryClient_LiveSearchServers tests SearchServers against the live GitHub MCP registry +func TestMCPRegistryClient_LiveSearchServers(t *testing.T) { + if testing.Short() { + t.Skip("Skipping live registry integration test in short mode") + } + + // Create client with default production registry URL + client := NewMCPRegistryClient(string(constants.DefaultMCPRegistryURL)) + + // Test 1: Search for all servers (empty query) + t.Run("search_all_servers", func(t *testing.T) { + servers, err := client.SearchServers("") + if err != nil { + // Check if it's a network/firewall issue + if strings.Contains(err.Error(), "network") || strings.Contains(err.Error(), "firewall") || + strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "connection") { + t.Skipf("Skipping due to network restrictions: %v", err) + return + } + t.Fatalf("SearchServers failed: %v", err) + } + + // The production registry should have multiple servers + if len(servers) < 10 { + t.Logf("Warning: Expected at least 10 servers from production registry, got %d", len(servers)) + t.Logf("This may indicate an issue with the registry or API changes") + } else { + t.Logf("✓ Successfully fetched %d servers from live registry", len(servers)) + } + + // Validate that servers have required fields + if len(servers) > 0 { + firstServer := servers[0] + if firstServer.Name == "" { + t.Errorf("First server has empty name") + } + if firstServer.Description == "" { + t.Errorf("First server has empty description") + } + if firstServer.Transport == "" { + t.Errorf("First server has empty transport") + } + t.Logf("✓ First server structure validated: name=%s, transport=%s", firstServer.Name, firstServer.Transport) + } + }) + + // Test 2: Search for specific servers by query + t.Run("search_with_query", func(t *testing.T) { + // Search for GitHub-related servers + servers, err := client.SearchServers("github") + if err != nil { + if strings.Contains(err.Error(), "network") || strings.Contains(err.Error(), "firewall") || + strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "connection") { + t.Skipf("Skipping due to network restrictions: %v", err) + return + } + t.Fatalf("SearchServers with query failed: %v", err) + } + + if len(servers) == 0 { + t.Errorf("Expected at least one server matching 'github', got none") + } else { + t.Logf("✓ Found %d servers matching 'github'", len(servers)) + // Verify that results match the query + for _, server := range servers { + lowerName := strings.ToLower(server.Name) + lowerDesc := strings.ToLower(server.Description) + if !strings.Contains(lowerName, "github") && !strings.Contains(lowerDesc, "github") { + t.Errorf("Server '%s' doesn't contain 'github' in name or description", server.Name) + } + } + } + }) + + // Test 3: Verify different transport types are supported + t.Run("verify_transport_types", func(t *testing.T) { + servers, err := client.SearchServers("") + if err != nil { + if strings.Contains(err.Error(), "network") || strings.Contains(err.Error(), "firewall") || + strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "connection") { + t.Skipf("Skipping due to network restrictions: %v", err) + return + } + t.Fatalf("SearchServers failed: %v", err) + } + + transportTypes := make(map[string]int) + for _, server := range servers { + transportTypes[server.Transport]++ + } + + t.Logf("✓ Found transport types: %v", transportTypes) + + // stdio should be the most common transport type + if transportTypes["stdio"] == 0 { + t.Errorf("Expected at least one server with stdio transport") + } + }) +} + +// TestMCPRegistryClient_LiveGetServer tests GetServer against the live GitHub MCP registry +func TestMCPRegistryClient_LiveGetServer(t *testing.T) { + if testing.Short() { + t.Skip("Skipping live registry integration test in short mode") + } + + // Create client with default production registry URL + client := NewMCPRegistryClient(string(constants.DefaultMCPRegistryURL)) + + // Test 1: Get a known server that should exist in the registry + // We'll use the GitHub MCP server as it's maintained by GitHub + t.Run("get_github_server", func(t *testing.T) { + // First, search for GitHub servers to find an actual server name + servers, err := client.SearchServers("github") + if err != nil { + if strings.Contains(err.Error(), "network") || strings.Contains(err.Error(), "firewall") || + strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "connection") { + t.Skipf("Skipping due to network restrictions: %v", err) + return + } + t.Fatalf("SearchServers failed: %v", err) + } + + if len(servers) == 0 { + t.Skip("No GitHub servers found in registry to test GetServer") + return + } + + // Get the first server's name + serverName := servers[0].Name + t.Logf("Testing GetServer with: %s", serverName) + + // Now test GetServer with that name + server, err := client.GetServer(serverName) + if err != nil { + t.Fatalf("GetServer failed for '%s': %v", serverName, err) + } + + // Validate the returned server + if server.Name != serverName { + t.Errorf("Expected server name '%s', got '%s'", serverName, server.Name) + } + if server.Description == "" { + t.Errorf("Server description is empty") + } + if server.Transport == "" { + t.Errorf("Server transport is empty") + } + + t.Logf("✓ Successfully retrieved server: %s", server.Name) + t.Logf(" Description: %s", server.Description) + t.Logf(" Transport: %s", server.Transport) + if server.Command != "" { + t.Logf(" Command: %s", server.Command) + } + if server.RuntimeHint != "" { + t.Logf(" Runtime Hint: %s", server.RuntimeHint) + } + }) + + // Test 2: Get a server that doesn't exist + t.Run("get_nonexistent_server", func(t *testing.T) { + _, err := client.GetServer("nonexistent/fake-server-12345") + if err == nil { + t.Errorf("Expected error for nonexistent server, got nil") + } + + expectedErrorSubstring := "not found in registry" + if !strings.Contains(err.Error(), expectedErrorSubstring) { + t.Errorf("Expected error to contain '%s', got: %s", expectedErrorSubstring, err.Error()) + } + + t.Logf("✓ Correctly returned error for nonexistent server") + }) +} + +// TestMCPRegistryClient_LiveResponseStructure tests that the v0.1 API structure is correctly parsed +func TestMCPRegistryClient_LiveResponseStructure(t *testing.T) { + if testing.Short() { + t.Skip("Skipping live registry integration test in short mode") + } + + // Create client with default production registry URL + client := NewMCPRegistryClient(string(constants.DefaultMCPRegistryURL)) + + servers, err := client.SearchServers("") + if err != nil { + if strings.Contains(err.Error(), "network") || strings.Contains(err.Error(), "firewall") || + strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "connection") { + t.Skipf("Skipping due to network restrictions: %v", err) + return + } + t.Fatalf("SearchServers failed: %v", err) + } + + if len(servers) == 0 { + t.Skip("No servers returned from registry") + return + } + + // Test that we can parse various fields correctly from the v0.1 structure + t.Run("validate_v0.1_fields", func(t *testing.T) { + hasPackageArgs := false + hasEnvVars := false + hasRuntimeHint := false + hasRepository := false + + for _, server := range servers { + // Check for package arguments + if len(server.Args) > 0 { + hasPackageArgs = true + t.Logf("✓ Found server with package arguments: %s", server.Name) + } + + // Check for environment variables + if len(server.EnvironmentVariables) > 0 { + hasEnvVars = true + t.Logf("✓ Found server with environment variables: %s (count: %d)", + server.Name, len(server.EnvironmentVariables)) + } + + // Check for runtime hint + if server.RuntimeHint != "" { + hasRuntimeHint = true + t.Logf("✓ Found server with runtime hint: %s (hint: %s)", + server.Name, server.RuntimeHint) + } + + // Check for repository + if server.Repository != "" { + hasRepository = true + } + } + + // Log what we found + if hasPackageArgs { + t.Logf("✓ Successfully parsed packageArguments from v0.1 API") + } + if hasEnvVars { + t.Logf("✓ Successfully parsed environmentVariables from v0.1 API") + } + if hasRuntimeHint { + t.Logf("✓ Successfully parsed runtimeHint from v0.1 API") + } + if hasRepository { + t.Logf("✓ Successfully parsed repository from v0.1 API") + } + + // At least some servers should have these fields + if !hasRuntimeHint { + t.Logf("Warning: No servers found with runtimeHint field") + } + }) + + // Test transport type parsing + t.Run("validate_transport_parsing", func(t *testing.T) { + for _, server := range servers { + // Transport should always be set + if server.Transport == "" { + t.Errorf("Server '%s' has empty transport", server.Name) + } + + // Transport should be one of the expected values + validTransports := map[string]bool{ + "stdio": true, + "sse": true, + "streamable-http": true, + } + + if !validTransports[server.Transport] { + t.Logf("Note: Server '%s' has unexpected transport type: %s", + server.Name, server.Transport) + } + } + t.Logf("✓ Transport types validated for %d servers", len(servers)) + }) +}