Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions cmd/thv/app/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@ Valid clients:
- antigravity: Google Antigravity IDE
- claude-code: Claude Code CLI
- cline: Cline extension for VS Code
- codex: OpenAI Codex CLI
- continue: Continue.dev extensions for VS Code and JetBrains
- cursor: Cursor editor
- gemini-cli: Google Gemini CLI
- goose: Goose AI agent
- kiro: Kiro AI IDE
- lm-studio: LM Studio application
- mistral-vibe: Mistral Vibe IDE
- opencode: OpenCode editor
- roo-code: Roo Code extension for VS Code
- trae: Trae IDE
Expand Down Expand Up @@ -91,12 +93,14 @@ Valid clients:
- antigravity: Google Antigravity IDE
- claude-code: Claude Code CLI
- cline: Cline extension for VS Code
- codex: OpenAI Codex CLI
- continue: Continue.dev extensions for VS Code and JetBrains
- cursor: Cursor editor
- gemini-cli: Google Gemini CLI
- goose: Goose AI agent
- kiro: Kiro AI IDE
- lm-studio: LM Studio application
- mistral-vibe: Mistral Vibe IDE
- opencode: OpenCode editor
- roo-code: Roo Code extension for VS Code
- trae: Trae IDE
Expand Down Expand Up @@ -213,13 +217,13 @@ func clientRegisterCmdFunc(cmd *cobra.Command, args []string) error {
switch clientType {
case "roo-code", "cline", "cursor", "claude-code", "vscode-insider", "vscode", "vscode-server", "windsurf", "windsurf-jetbrains",
"amp-cli", "amp-vscode", "amp-vscode-insider", "amp-cursor", "amp-windsurf", "lm-studio", "goose", "trae",
"continue", "opencode", "kiro", "antigravity", "zed", "gemini-cli":
"continue", "opencode", "kiro", "antigravity", "zed", "gemini-cli", "mistral-vibe", "codex":
// Valid client type
default:
return fmt.Errorf(
"invalid client type: %s (valid types: roo-code, cline, cursor, claude-code, vscode, vscode-insider, vscode-server, "+
"windsurf, windsurf-jetbrains, amp-cli, amp-vscode, amp-vscode-insider, amp-cursor, amp-windsurf, lm-studio, "+
"goose, trae, continue, opencode, kiro, antigravity, zed, gemini-cli)",
"goose, trae, continue, opencode, kiro, antigravity, zed, gemini-cli, mistral-vibe, codex)",
clientType)
}

Expand All @@ -233,13 +237,13 @@ func clientRemoveCmdFunc(cmd *cobra.Command, args []string) error {
switch clientType {
case "roo-code", "cline", "cursor", "claude-code", "vscode-insider", "vscode", "vscode-server", "windsurf", "windsurf-jetbrains",
"amp-cli", "amp-vscode", "amp-vscode-insider", "amp-cursor", "amp-windsurf", "lm-studio", "goose", "trae",
"continue", "opencode", "kiro", "antigravity", "zed", "gemini-cli":
"continue", "opencode", "kiro", "antigravity", "zed", "gemini-cli", "mistral-vibe", "codex":
// Valid client type
default:
return fmt.Errorf(
"invalid client type: %s (valid types: roo-code, cline, cursor, claude-code, vscode, vscode-insider, vscode-server, "+
"windsurf, windsurf-jetbrains, amp-cli, amp-vscode, amp-vscode-insider, amp-cursor, amp-windsurf, lm-studio, "+
"goose, trae, continue, opencode, kiro, antigravity, zed, gemini-cli)",
"goose, trae, continue, opencode, kiro, antigravity, zed, gemini-cli, mistral-vibe, codex)",
clientType)
}

Expand Down
2 changes: 2 additions & 0 deletions docs/cli/thv_client_register.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/cli/thv_client_remove.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions docs/server/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions docs/server/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions docs/server/swagger.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions pkg/client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ const (
GeminiCli MCPClient = "gemini-cli"
// VSCodeServer represents Microsoft's VS Code Server (remote development).
VSCodeServer MCPClient = "vscode-server"
// MistralVibe represents the Mistral Vibe IDE.
MistralVibe MCPClient = "mistral-vibe"
// Codex represents the OpenAI Codex CLI.
Codex MCPClient = "codex"
)

// Extension is extension of the client config file.
Expand Down Expand Up @@ -665,12 +669,44 @@ var supportedClientIntegrations = []mcpClientConfig{
types.TransportTypeSSE: "sse",
types.TransportTypeStreamableHTTP: "http",
},
},
{
ClientType: MistralVibe,
Description: "Mistral Vibe IDE",
SettingsFile: "config.toml",
MCPServersPathPrefix: "/mcp_servers",
RelPath: []string{".vibe"},
Extension: TOML,
SupportedTransportTypesMap: map[types.TransportType]string{
types.TransportTypeStdio: "streamable-http",
types.TransportTypeSSE: "http",
types.TransportTypeStreamableHTTP: "streamable-http",
},
IsTransportTypeFieldSupported: true,
MCPServersUrlLabelMap: map[types.TransportType]string{
types.TransportTypeStdio: "url",
types.TransportTypeSSE: "url",
types.TransportTypeStreamableHTTP: "url",
},
// TOML configuration: uses array-of-tables format [[mcp_servers]]
TOMLStorageType: TOMLStorageTypeArray,
},
{
ClientType: Codex,
Description: "OpenAI Codex CLI",
SettingsFile: "config.toml",
MCPServersPathPrefix: "/mcp_servers",
RelPath: []string{".codex"},
Extension: TOML,
// Codex doesn't support a transport type field - it auto-detects
IsTransportTypeFieldSupported: false,
MCPServersUrlLabelMap: map[types.TransportType]string{
types.TransportTypeStdio: "url",
types.TransportTypeSSE: "url",
types.TransportTypeStreamableHTTP: "url",
},
// TOML configuration: uses nested tables format [mcp_servers.servername]
TOMLStorageType: TOMLStorageTypeMap,
},
}

Expand Down
140 changes: 139 additions & 1 deletion pkg/client/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,24 @@ func createMockClientConfigs() []mcpClientConfig {
MCPServersPathPrefix: "/servers",
Extension: JSON,
},
{
ClientType: MistralVibe,
Description: "Mistral Vibe IDE (Mock)",
RelPath: []string{"mock_mistral_vibe"},
SettingsFile: "config.toml",
MCPServersPathPrefix: "/mcp_servers",
Extension: TOML,
TOMLStorageType: TOMLStorageTypeArray,
},
{
ClientType: Codex,
Description: "OpenAI Codex CLI (Mock)",
RelPath: []string{"mock_codex"},
SettingsFile: "config.toml",
MCPServersPathPrefix: "/mcp_servers",
Extension: TOML,
TOMLStorageType: TOMLStorageTypeMap,
},
}
}

Expand Down Expand Up @@ -380,6 +398,8 @@ func TestSuccessfulClientConfigOperations(t *testing.T) {
string(Zed),
string(GeminiCli),
string(VSCodeServer),
string(MistralVibe),
string(Codex),
},
},
}
Expand Down Expand Up @@ -492,6 +512,10 @@ func TestSuccessfulClientConfigOperations(t *testing.T) {
// YAML files are created empty and initialized on first use
// Just verify the file exists and is readable
assert.NotNil(t, content, "Continue config should be readable")
case MistralVibe, Codex:
// TOML files are created empty and initialized on first use
// Just verify the file exists and is readable
assert.NotNil(t, content, "TOML config should be readable")
}
}
})
Expand Down Expand Up @@ -526,7 +550,8 @@ func TestSuccessfulClientConfigOperations(t *testing.T) {
assert.Contains(t, string(content), testURL,
"VSCode config should contain the server URL")
case Cursor, RooCode, ClaudeCode, Cline, Windsurf, WindsurfJetBrains, AmpCli,
AmpVSCode, AmpCursor, AmpVSCodeInsider, AmpWindsurf, LMStudio, Goose, Trae, Continue, OpenCode, Kiro, Antigravity, Zed, GeminiCli, VSCodeServer:
AmpVSCode, AmpCursor, AmpVSCodeInsider, AmpWindsurf, LMStudio, Goose, Trae, Continue, OpenCode, Kiro, Antigravity, Zed, GeminiCli, VSCodeServer,
MistralVibe, Codex:
assert.Contains(t, string(content), testURL,
"Config should contain the server URL")
}
Expand Down Expand Up @@ -796,6 +821,119 @@ func TestCreateClientConfig(t *testing.T) {
})
}

func TestCreateTOMLClientConfig(t *testing.T) {
t.Parallel()
logger.Initialize()

testConfig := &config.Config{
Secrets: config.Secrets{
ProviderType: "encrypted",
},
Clients: config.Clients{
RegisteredClients: []string{
string(MistralVibe),
string(Codex),
},
},
}

t.Run("CreateTOMLArrayClientConfig", func(t *testing.T) {
t.Parallel()
// Setup a temporary home directory for testing
tempHome := t.TempDir()

configProvider, cleanup := CreateTestConfigProvider(t, testConfig)
defer cleanup()

// Create mock client config for TOML client with array storage (MistralVibe)
mockClientConfigs := []mcpClientConfig{
{
ClientType: MistralVibe,
Description: "Mistral Vibe IDE (Mock)",
RelPath: []string{"mock_mistral_vibe"},
SettingsFile: "config.toml",
MCPServersPathPrefix: "/mcp_servers",
Extension: TOML,
TOMLStorageType: TOMLStorageTypeArray,
},
}

// Create the parent directory structure that would normally exist
configDir := filepath.Join(tempHome, "mock_mistral_vibe")
err := os.MkdirAll(configDir, 0755)
require.NoError(t, err)

manager := NewTestClientManager(tempHome, nil, mockClientConfigs, configProvider)

// Call CreateClientConfig - this should create a new TOML file
cf, err := manager.CreateClientConfig(MistralVibe)
require.NoError(t, err, "Should successfully create new TOML client config")
require.NotNil(t, cf, "Should return a config file")

// Verify the file was created
_, statErr := os.Stat(cf.Path)
require.NoError(t, statErr, "Config file should exist after creation")

// Verify the file is empty (TOML files start empty like YAML)
content, err := os.ReadFile(cf.Path)
require.NoError(t, err, "Should be able to read created file")
assert.Equal(t, "", string(content), "TOML config should be empty initially")

// Verify file permissions
fileInfo, err := os.Stat(cf.Path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0600), fileInfo.Mode().Perm(), "File should have 0600 permissions")
})

t.Run("CreateTOMLMapClientConfig", func(t *testing.T) {
t.Parallel()
// Setup a temporary home directory for testing
tempHome := t.TempDir()

configProvider, cleanup := CreateTestConfigProvider(t, testConfig)
defer cleanup()

// Create mock client config for TOML client with map storage (Codex)
mockClientConfigs := []mcpClientConfig{
{
ClientType: Codex,
Description: "OpenAI Codex CLI (Mock)",
RelPath: []string{"mock_codex"},
SettingsFile: "config.toml",
MCPServersPathPrefix: "/mcp_servers",
Extension: TOML,
TOMLStorageType: TOMLStorageTypeMap,
},
}

// Create the parent directory structure that would normally exist
configDir := filepath.Join(tempHome, "mock_codex")
err := os.MkdirAll(configDir, 0755)
require.NoError(t, err)

manager := NewTestClientManager(tempHome, nil, mockClientConfigs, configProvider)

// Call CreateClientConfig - this should create a new TOML file
cf, err := manager.CreateClientConfig(Codex)
require.NoError(t, err, "Should successfully create new TOML client config")
require.NotNil(t, cf, "Should return a config file")

// Verify the file was created
_, statErr := os.Stat(cf.Path)
require.NoError(t, statErr, "Config file should exist after creation")

// Verify the file is empty (TOML files start empty like YAML)
content, err := os.ReadFile(cf.Path)
require.NoError(t, err, "Should be able to read created file")
assert.Equal(t, "", string(content), "TOML config should be empty initially")

// Verify file permissions
fileInfo, err := os.Stat(cf.Path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0600), fileInfo.Mode().Perm(), "File should have 0600 permissions")
})
}

func TestUpsertWithDynamicUrlFieldMapping(t *testing.T) {
t.Parallel()
logger.Initialize()
Expand Down
Loading