Skip to content

Commit da6bbab

Browse files
authored
move client configuration commands under thv client (#1040)
1 parent c593aa5 commit da6bbab

File tree

9 files changed

+292
-178
lines changed

9 files changed

+292
-178
lines changed

cmd/thv/app/client.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/stacklok/toolhive/cmd/thv/app/ui"
99
"github.com/stacklok/toolhive/pkg/client"
10+
"github.com/stacklok/toolhive/pkg/config"
1011
)
1112

1213
var clientCmd = &cobra.Command{
@@ -29,11 +30,51 @@ var clientSetupCmd = &cobra.Command{
2930
RunE: clientSetupCmdFunc,
3031
}
3132

33+
var clientRegisterCmd = &cobra.Command{
34+
Use: "register [client]",
35+
Short: "Register a client for MCP server configuration",
36+
Long: `Register a client for MCP server configuration.
37+
Valid clients are:
38+
- claude-code: Claude Code CLI
39+
- cline: Cline extension for VS Code
40+
- cursor: Cursor editor
41+
- roo-code: Roo Code extension for VS Code
42+
- vscode: Visual Studio Code
43+
- vscode-insider: Visual Studio Code Insiders edition`,
44+
Args: cobra.ExactArgs(1),
45+
RunE: clientRegisterCmdFunc,
46+
}
47+
48+
var clientRemoveCmd = &cobra.Command{
49+
Use: "remove [client]",
50+
Short: "Remove a client from MCP server configuration",
51+
Long: `Remove a client from MCP server configuration.
52+
Valid clients are:
53+
- claude-code: Claude Code CLI
54+
- cline: Cline extension for VS Code
55+
- cursor: Cursor editor
56+
- roo-code: Roo Code extension for VS Code
57+
- vscode: Visual Studio Code
58+
- vscode-insider: Visual Studio Code Insiders edition`,
59+
Args: cobra.ExactArgs(1),
60+
RunE: clientRemoveCmdFunc,
61+
}
62+
63+
var clientListRegisteredCmd = &cobra.Command{
64+
Use: "list-registered",
65+
Short: "List all registered MCP clients",
66+
Long: "List all clients that are registered for MCP server configuration.",
67+
RunE: listRegisteredClientsCmdFunc,
68+
}
69+
3270
func init() {
3371
rootCmd.AddCommand(clientCmd)
3472

3573
clientCmd.AddCommand(clientStatusCmd)
3674
clientCmd.AddCommand(clientSetupCmd)
75+
clientCmd.AddCommand(clientRegisterCmd)
76+
clientCmd.AddCommand(clientRemoveCmd)
77+
clientCmd.AddCommand(clientListRegisteredCmd)
3778
}
3879

3980
func clientStatusCmdFunc(_ *cobra.Command, _ []string) error {
@@ -101,3 +142,76 @@ func registerSelectedClients(cmd *cobra.Command, clientsToRegister []client.MCPC
101142

102143
return nil
103144
}
145+
146+
func clientRegisterCmdFunc(cmd *cobra.Command, args []string) error {
147+
clientType := args[0]
148+
149+
// Validate the client type
150+
switch clientType {
151+
case "roo-code", "cline", "cursor", "claude-code", "vscode-insider", "vscode":
152+
// Valid client type
153+
default:
154+
return fmt.Errorf(
155+
"invalid client type: %s (valid types: roo-code, cline, cursor, claude-code, vscode, vscode-insider)",
156+
clientType)
157+
}
158+
159+
ctx := cmd.Context()
160+
161+
manager, err := client.NewManager(ctx)
162+
if err != nil {
163+
return fmt.Errorf("failed to create client manager: %w", err)
164+
}
165+
166+
err = manager.RegisterClients(ctx, []client.Client{
167+
{Name: client.MCPClient(clientType)},
168+
})
169+
if err != nil {
170+
return fmt.Errorf("failed to register client %s: %w", clientType, err)
171+
}
172+
173+
return nil
174+
}
175+
176+
func clientRemoveCmdFunc(cmd *cobra.Command, args []string) error {
177+
clientType := args[0]
178+
179+
// Validate the client type
180+
switch clientType {
181+
case "roo-code", "cline", "cursor", "claude-code", "vscode-insider", "vscode":
182+
// Valid client type
183+
default:
184+
return fmt.Errorf(
185+
"invalid client type: %s (valid types: roo-code, cline, cursor, claude-code, vscode, vscode-insider)",
186+
clientType)
187+
}
188+
189+
ctx := cmd.Context()
190+
191+
manager, err := client.NewManager(ctx)
192+
if err != nil {
193+
return fmt.Errorf("failed to create client manager: %w", err)
194+
}
195+
196+
err = manager.UnregisterClients(ctx, []client.Client{
197+
{Name: client.MCPClient(clientType)},
198+
})
199+
if err != nil {
200+
return fmt.Errorf("failed to remove client %s: %w", clientType, err)
201+
}
202+
203+
return nil
204+
}
205+
206+
func listRegisteredClientsCmdFunc(_ *cobra.Command, _ []string) error {
207+
cfg := config.GetConfig()
208+
if len(cfg.Clients.RegisteredClients) == 0 {
209+
fmt.Println("No clients are currently registered.")
210+
return nil
211+
}
212+
fmt.Println("Registered clients:")
213+
for _, clientName := range cfg.Clients.RegisteredClients {
214+
fmt.Printf(" - %s\n", clientName)
215+
}
216+
return nil
217+
}

cmd/thv/app/client_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package app
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"strings"
7+
"testing"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/stretchr/testify/assert"
11+
12+
"github.com/stacklok/toolhive/pkg/config"
13+
)
14+
15+
func registerClientViaCLI(cmd *cobra.Command, client string) error {
16+
cmd.SetOut(&bytes.Buffer{})
17+
cmd.SetErr(&bytes.Buffer{})
18+
cmd.SetArgs([]string{"client", "register", client})
19+
return cmd.Execute()
20+
}
21+
22+
func removeClientViaCLI(cmd *cobra.Command, client string) error {
23+
cmd.SetOut(&bytes.Buffer{})
24+
cmd.SetErr(&bytes.Buffer{})
25+
cmd.SetArgs([]string{"client", "remove", client})
26+
return cmd.Execute()
27+
}
28+
29+
func TestClientRegisterCmd(t *testing.T) {
30+
t.Parallel()
31+
tempDir := t.TempDir()
32+
os.Setenv("XDG_CONFIG_HOME", tempDir)
33+
34+
cmd := rootCmd
35+
36+
err := registerClientViaCLI(cmd, "vscode")
37+
assert.NoError(t, err)
38+
39+
cfg := config.GetConfig()
40+
assert.Contains(t, cfg.Clients.RegisteredClients, "vscode", "Client should be registered")
41+
}
42+
43+
func TestClientRemoveCmd(t *testing.T) {
44+
t.Parallel()
45+
tempDir := t.TempDir()
46+
os.Setenv("XDG_CONFIG_HOME", tempDir)
47+
48+
// Pre-populate config with a registered client
49+
err := config.UpdateConfig(func(c *config.Config) {
50+
c.Clients.RegisteredClients = []string{"vscode"}
51+
})
52+
assert.NoError(t, err)
53+
54+
cmd := rootCmd
55+
56+
err = removeClientViaCLI(cmd, "vscode")
57+
assert.NoError(t, err)
58+
59+
cfg := config.GetConfig()
60+
assert.NotContains(t, cfg.Clients.RegisteredClients, "vscode", "Client should be removed")
61+
}
62+
63+
func TestClientRegisterCmd_InvalidClient(t *testing.T) {
64+
t.Parallel()
65+
tempDir := t.TempDir()
66+
os.Setenv("XDG_CONFIG_HOME", tempDir)
67+
68+
cmd := rootCmd
69+
70+
err := registerClientViaCLI(cmd, "not-a-client")
71+
assert.Error(t, err)
72+
assert.True(t, strings.Contains(err.Error(), "invalid client type"))
73+
}
74+
75+
func TestClientRemoveCmd_InvalidClient(t *testing.T) {
76+
t.Parallel()
77+
tempDir := t.TempDir()
78+
os.Setenv("XDG_CONFIG_HOME", tempDir)
79+
80+
cmd := rootCmd
81+
82+
err := removeClientViaCLI(cmd, "not-a-client")
83+
assert.Error(t, err)
84+
assert.True(t, strings.Contains(err.Error(), "invalid client type"))
85+
}

0 commit comments

Comments
 (0)