diff --git a/docs/cli/thv_client_register.md b/docs/cli/thv_client_register.md index 0ce5cfeb50..0cef577fba 100644 --- a/docs/cli/thv_client_register.md +++ b/docs/cli/thv_client_register.md @@ -34,6 +34,7 @@ Valid clients: - lm-studio: LM Studio application - mistral-vibe: Mistral Vibe IDE - opencode: OpenCode editor + - qoder: Qoder IDE - roo-code: VS Code Roo Code extension (deprecated) - trae: Trae IDE - vscode: Visual Studio Code diff --git a/docs/cli/thv_client_remove.md b/docs/cli/thv_client_remove.md index c5bb53a5e1..52ec371f4c 100644 --- a/docs/cli/thv_client_remove.md +++ b/docs/cli/thv_client_remove.md @@ -34,6 +34,7 @@ Valid clients: - lm-studio: LM Studio application - mistral-vibe: Mistral Vibe IDE - opencode: OpenCode editor + - qoder: Qoder IDE - roo-code: VS Code Roo Code extension (deprecated) - trae: Trae IDE - vscode: Visual Studio Code diff --git a/docs/server/docs.go b/docs/server/docs.go index 7fc18c2f00..9aa4269bc4 100644 --- a/docs/server/docs.go +++ b/docs/server/docs.go @@ -922,7 +922,8 @@ const docTemplate = `{ "codex", "kimi-cli", "factory", - "copilot-cli" + "copilot-cli", + "qoder" ], "type": "string", "x-enum-varnames": [ @@ -949,7 +950,8 @@ const docTemplate = `{ "Codex", "KimiCli", "Factory", - "CopilotCli" + "CopilotCli", + "Qoder" ] }, "github_com_stacklok_toolhive_pkg_client.ClientAppStatus": { diff --git a/docs/server/swagger.json b/docs/server/swagger.json index a43404b4eb..5893fcbc49 100644 --- a/docs/server/swagger.json +++ b/docs/server/swagger.json @@ -915,7 +915,8 @@ "codex", "kimi-cli", "factory", - "copilot-cli" + "copilot-cli", + "qoder" ], "type": "string", "x-enum-varnames": [ @@ -942,7 +943,8 @@ "Codex", "KimiCli", "Factory", - "CopilotCli" + "CopilotCli", + "Qoder" ] }, "github_com_stacklok_toolhive_pkg_client.ClientAppStatus": { diff --git a/docs/server/swagger.yaml b/docs/server/swagger.yaml index 3ba6e5b194..9cd9c5ff03 100644 --- a/docs/server/swagger.yaml +++ b/docs/server/swagger.yaml @@ -1003,6 +1003,7 @@ components: - kimi-cli - factory - copilot-cli + - qoder type: string x-enum-varnames: - RooCode @@ -1029,6 +1030,7 @@ components: - KimiCli - Factory - CopilotCli + - Qoder github_com_stacklok_toolhive_pkg_client.ClientAppStatus: properties: client_type: diff --git a/pkg/client/config.go b/pkg/client/config.go index 613f8405ad..9f7239bd2c 100644 --- a/pkg/client/config.go +++ b/pkg/client/config.go @@ -93,6 +93,8 @@ const ( Factory ClientApp = "factory" // CopilotCli represents the GitHub Copilot CLI. CopilotCli ClientApp = "copilot-cli" + // Qoder represents the Qoder IDE. + Qoder ClientApp = "qoder" ) const ( @@ -1022,6 +1024,33 @@ var supportedClientIntegrations = []clientAppConfig{ {JSONPointer: "/apiKey", ValueField: "PlaceholderAPIKey"}, }, }, + { + ClientType: ClientApp(Qoder), + Description: "Qoder IDE", + SettingsFile: "mcp.json", + MCPServersPathPrefix: "/mcpServers", + RelPath: []string{}, + PlatformPrefix: map[Platform][]string{ + PlatformDarwin: {".qoder"}, + PlatformLinux: {".qoder"}, + PlatformWindows: {"AppData", "Roaming", "Qoder", "SharedClientCache"}, + }, + Extension: JSON, + IsTransportTypeFieldSupported: true, + SupportedTransportTypesMap: map[types.TransportType]string{ + types.TransportTypeStdio: httpTransportLabel, + types.TransportTypeSSE: "sse", + types.TransportTypeStreamableHTTP: httpTransportLabel, + }, + MCPServersUrlLabelMap: map[types.TransportType]string{ + types.TransportTypeStdio: defaultURLFieldName, + types.TransportTypeSSE: defaultURLFieldName, + types.TransportTypeStreamableHTTP: defaultURLFieldName, + }, + SupportsSkills: true, + SkillsGlobalPath: []string{".agents", skillsDirName}, + SkillsProjectPath: []string{".qoder", skillsDirName}, + }, { // Claude Desktop routes LLM traffic through the gateway via its // "third-party inference" surface. Unlike Claude Code (a single JSON diff --git a/pkg/client/config_test.go b/pkg/client/config_test.go index 0daffd239a..2672d8fd07 100644 --- a/pkg/client/config_test.go +++ b/pkg/client/config_test.go @@ -193,6 +193,14 @@ func createMockClientConfigs() []clientAppConfig { MCPServersPathPrefix: "/mcpServers", Extension: JSON, }, + { + ClientType: Qoder, + Description: "Qoder (Mock)", + RelPath: []string{"mock_qoder"}, + SettingsFile: "mcp.json", + MCPServersPathPrefix: "/mcpServers", + Extension: JSON, + }, } } @@ -376,6 +384,7 @@ func TestSuccessfulClientConfigOperations(t *testing.T) { string(Codex), string(KimiCli), string(Factory), + string(Qoder), }, }, } @@ -456,7 +465,7 @@ func TestSuccessfulClientConfigOperations(t *testing.T) { case AmpCli: assert.Contains(t, string(content), `"mcpServers":`, "AmpCli config should contain mcpServers key") - case LMStudio, Trae, Kiro, Antigravity, GeminiCli, KimiCli, Factory, CopilotCli: + case Qoder, LMStudio, Trae, Kiro, Antigravity, GeminiCli, KimiCli, Factory, CopilotCli: assert.Contains(t, string(content), `"mcpServers":`, "Config should contain mcpServers key") case VSCodeServer: @@ -515,7 +524,7 @@ func TestSuccessfulClientConfigOperations(t *testing.T) { "VSCode config should contain the server URL") case Cursor, RooCode, ClaudeCode, Cline, Windsurf, WindsurfJetBrains, AmpCli, LMStudio, Goose, Trae, Continue, OpenCode, Kiro, Antigravity, Zed, GeminiCli, VSCodeServer, - MistralVibe, Codex, KimiCli, Factory, CopilotCli: + MistralVibe, Codex, Qoder, KimiCli, Factory, CopilotCli: assert.Contains(t, string(content), testURL, "Config should contain the server URL") } @@ -1244,8 +1253,8 @@ func TestGetAllClients(t *testing.T) { clients := GetAllClients() - // Should return all 24 supported clients - assert.Len(t, clients, 24, "Expected 24 supported clients") + // Should return all 25 supported clients + assert.Len(t, clients, 25, "Expected 25 supported clients") // Verify the list is sorted alphabetically for i := 1; i < len(clients); i++ { @@ -1459,7 +1468,7 @@ func TestGetClientListCSV(t *testing.T) { clientNames[i-1], clientNames[i]) } - // Count the number of clients (should be 24) + // Count the number of clients (should be 25) clients := strings.Split(csv, ", ") - assert.Len(t, clients, 24, "Expected 24 clients in CSV list") + assert.Len(t, clients, 25, "Expected 25 clients in CSV list") } diff --git a/pkg/client/discovery.go b/pkg/client/discovery.go index 5173b2dd24..c0a6157d4a 100644 --- a/pkg/client/discovery.go +++ b/pkg/client/discovery.go @@ -113,7 +113,7 @@ func (cm *ClientManager) IsClientInstalled(clientType ClientApp) bool { return false } var pathToCheck string - if len(cfg.RelPath) == 0 { + if len(cfg.RelPath) == 0 && len(cfg.PlatformPrefix) == 0 { pathToCheck = filepath.Join(cm.homeDir, cfg.SettingsFile) } else { pathToCheck = buildConfigDirectoryPath(cfg.RelPath, cfg.PlatformPrefix, []string{cm.homeDir})