Skip to content

Commit 82718be

Browse files
authored
feat(040): Add/Edit Server UX improvements for macOS tray app
PATCH endpoint, editable config, inline validation, connection test feedback, quarantine auto-connect for tool review, docker isolation default ON, approve/quarantine buttons, socket fixes
1 parent 5644415 commit 82718be

52 files changed

Lines changed: 6561 additions & 1054 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/mcpproxy/connect_cmd.go

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/spf13/cobra"
8+
9+
clioutput "github.com/smart-mcp-proxy/mcpproxy-go/internal/cli/output"
10+
"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
11+
"github.com/smart-mcp-proxy/mcpproxy-go/internal/connect"
12+
)
13+
14+
var (
15+
connectList bool
16+
connectAll bool
17+
connectForce bool
18+
connectServerName string
19+
)
20+
21+
// GetConnectCommand returns the connect parent command.
22+
func GetConnectCommand() *cobra.Command {
23+
cmd := &cobra.Command{
24+
Use: "connect [client]",
25+
Short: "Register MCPProxy in a client's MCP configuration",
26+
Long: `Register MCPProxy as an MCP server in the configuration file of supported
27+
AI coding clients. This modifies the client's config file to add an HTTP/SSE
28+
entry pointing to the running MCPProxy instance.
29+
30+
Supported clients: claude-code, cursor, windsurf, vscode, codex, gemini
31+
32+
A backup of the original config file is created before any modification.
33+
34+
Examples:
35+
mcpproxy connect --list # Show all clients and their status
36+
mcpproxy connect claude-code # Register in Claude Code
37+
mcpproxy connect cursor --force # Overwrite existing entry
38+
mcpproxy connect codex --name my-proxy # Custom server name
39+
mcpproxy connect --all # Register in all supported clients`,
40+
Args: cobra.MaximumNArgs(1),
41+
RunE: runConnect,
42+
}
43+
44+
cmd.Flags().BoolVar(&connectList, "list", false, "List all clients and their connection status")
45+
cmd.Flags().BoolVar(&connectAll, "all", false, "Connect to all supported clients")
46+
cmd.Flags().BoolVar(&connectForce, "force", false, "Overwrite existing entry")
47+
cmd.Flags().StringVar(&connectServerName, "name", "", "Server name in client config (default: mcpproxy)")
48+
49+
return cmd
50+
}
51+
52+
// GetDisconnectCommand returns the disconnect command.
53+
func GetDisconnectCommand() *cobra.Command {
54+
cmd := &cobra.Command{
55+
Use: "disconnect <client>",
56+
Short: "Remove MCPProxy from a client's MCP configuration",
57+
Long: `Remove the MCPProxy entry from the specified client's configuration file.
58+
A backup of the original config file is created before any modification.
59+
60+
Examples:
61+
mcpproxy disconnect claude-code
62+
mcpproxy disconnect cursor --name my-proxy`,
63+
Args: cobra.ExactArgs(1),
64+
RunE: runDisconnect,
65+
}
66+
67+
cmd.Flags().StringVar(&connectServerName, "name", "", "Server name to remove (default: mcpproxy)")
68+
69+
return cmd
70+
}
71+
72+
func runConnect(cmd *cobra.Command, args []string) error {
73+
cfg, err := loadConnectConfig()
74+
if err != nil {
75+
return fmt.Errorf("failed to load config: %w", err)
76+
}
77+
78+
svc := connect.NewService(cfg.Listen, cfg.APIKey)
79+
80+
format := clioutput.ResolveFormat(globalOutputFormat, globalJSONOutput)
81+
formatter, err := clioutput.NewFormatter(format)
82+
if err != nil {
83+
return err
84+
}
85+
86+
// --list mode
87+
if connectList {
88+
return printConnectStatus(svc, formatter, format)
89+
}
90+
91+
// --all mode
92+
if connectAll {
93+
return connectAllClients(svc, formatter, format)
94+
}
95+
96+
// Single client mode
97+
if len(args) == 0 {
98+
return fmt.Errorf("client ID is required (or use --list / --all)")
99+
}
100+
101+
clientID := args[0]
102+
result, err := svc.Connect(clientID, connectServerName, connectForce)
103+
if err != nil {
104+
return err
105+
}
106+
107+
return printConnectResult(result, formatter, format)
108+
}
109+
110+
func runDisconnect(cmd *cobra.Command, args []string) error {
111+
cfg, err := loadConnectConfig()
112+
if err != nil {
113+
return fmt.Errorf("failed to load config: %w", err)
114+
}
115+
116+
svc := connect.NewService(cfg.Listen, cfg.APIKey)
117+
118+
format := clioutput.ResolveFormat(globalOutputFormat, globalJSONOutput)
119+
formatter, err := clioutput.NewFormatter(format)
120+
if err != nil {
121+
return err
122+
}
123+
124+
clientID := args[0]
125+
result, err := svc.Disconnect(clientID, connectServerName)
126+
if err != nil {
127+
return err
128+
}
129+
130+
return printConnectResult(result, formatter, format)
131+
}
132+
133+
func printConnectStatus(svc *connect.Service, formatter clioutput.OutputFormatter, format string) error {
134+
statuses := svc.GetAllStatus()
135+
136+
if format == "table" {
137+
headers := []string{"CLIENT", "STATUS", "CONFIG PATH", "CONNECTED"}
138+
var rows [][]string
139+
for _, s := range statuses {
140+
status := "supported"
141+
if !s.Supported {
142+
status = "unsupported"
143+
}
144+
145+
connected := "-"
146+
if s.Supported {
147+
if s.Connected {
148+
connected = "yes"
149+
} else if s.Exists {
150+
connected = "no"
151+
} else {
152+
connected = "no (no config)"
153+
}
154+
}
155+
156+
cfgPath := s.ConfigPath
157+
if len(cfgPath) > 50 {
158+
cfgPath = "..." + cfgPath[len(cfgPath)-47:]
159+
}
160+
161+
rows = append(rows, []string{s.Name, status, cfgPath, connected})
162+
}
163+
out, err := formatter.FormatTable(headers, rows)
164+
if err != nil {
165+
return err
166+
}
167+
fmt.Print(out)
168+
return nil
169+
}
170+
171+
// JSON or YAML
172+
out, err := formatter.Format(statuses)
173+
if err != nil {
174+
return err
175+
}
176+
fmt.Println(out)
177+
return nil
178+
}
179+
180+
func connectAllClients(svc *connect.Service, formatter clioutput.OutputFormatter, format string) error {
181+
clients := connect.GetAllClients()
182+
var results []*connect.ConnectResult
183+
var errors []string
184+
185+
for _, c := range clients {
186+
if !c.Supported {
187+
continue
188+
}
189+
result, err := svc.Connect(c.ID, connectServerName, connectForce)
190+
if err != nil {
191+
errors = append(errors, fmt.Sprintf("%s: %v", c.Name, err))
192+
continue
193+
}
194+
results = append(results, result)
195+
}
196+
197+
if format == "table" {
198+
headers := []string{"CLIENT", "ACTION", "MESSAGE"}
199+
var rows [][]string
200+
for _, r := range results {
201+
client := connect.FindClient(r.Client)
202+
name := r.Client
203+
if client != nil {
204+
name = client.Name
205+
}
206+
rows = append(rows, []string{name, r.Action, r.Message})
207+
}
208+
for _, e := range errors {
209+
parts := strings.SplitN(e, ": ", 2)
210+
msg := e
211+
clientName := "unknown"
212+
if len(parts) == 2 {
213+
clientName = parts[0]
214+
msg = parts[1]
215+
}
216+
rows = append(rows, []string{clientName, "error", msg})
217+
}
218+
out, err := formatter.FormatTable(headers, rows)
219+
if err != nil {
220+
return err
221+
}
222+
fmt.Print(out)
223+
return nil
224+
}
225+
226+
// JSON/YAML output
227+
out, err := formatter.Format(map[string]interface{}{
228+
"results": results,
229+
"errors": errors,
230+
})
231+
if err != nil {
232+
return err
233+
}
234+
fmt.Println(out)
235+
return nil
236+
}
237+
238+
func printConnectResult(result *connect.ConnectResult, formatter clioutput.OutputFormatter, format string) error {
239+
if format == "table" {
240+
if result.Success {
241+
fmt.Printf("%s\n", result.Message)
242+
if result.BackupPath != "" {
243+
fmt.Printf("Backup: %s\n", result.BackupPath)
244+
}
245+
fmt.Printf("Config: %s\n", result.ConfigPath)
246+
} else {
247+
fmt.Printf("Failed: %s\n", result.Message)
248+
}
249+
return nil
250+
}
251+
252+
// JSON/YAML
253+
out, err := formatter.Format(result)
254+
if err != nil {
255+
return err
256+
}
257+
fmt.Println(out)
258+
return nil
259+
}
260+
261+
func loadConnectConfig() (*config.Config, error) {
262+
if configFile != "" {
263+
cfg, err := config.LoadFromFile(configFile)
264+
if err != nil {
265+
return nil, err
266+
}
267+
if dataDir != "" {
268+
cfg.DataDir = dataDir
269+
}
270+
return cfg, nil
271+
}
272+
cfg, err := config.Load()
273+
if err != nil {
274+
return nil, err
275+
}
276+
if dataDir != "" {
277+
cfg.DataDir = dataDir
278+
}
279+
return cfg, nil
280+
}

cmd/mcpproxy/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ func main() {
178178
// Add feedback command (Spec 036)
179179
feedbackCmd := GetFeedbackCommand()
180180

181+
// Add connect/disconnect commands
182+
connectCmd := GetConnectCommand()
183+
disconnectCmd := GetDisconnectCommand()
184+
181185
// Add commands to root
182186
rootCmd.AddCommand(serverCmd)
183187
rootCmd.AddCommand(searchCmd)
@@ -195,6 +199,8 @@ func main() {
195199
rootCmd.AddCommand(tokenCmd)
196200
rootCmd.AddCommand(telemetryCmd)
197201
rootCmd.AddCommand(feedbackCmd)
202+
rootCmd.AddCommand(connectCmd)
203+
rootCmd.AddCommand(disconnectCmd)
198204

199205
// Setup --help-json for machine-readable help discovery
200206
// This must be called AFTER all commands are added

frontend/src/App.vue

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@
99

1010
<!-- Page content -->
1111
<main class="overflow-y-auto p-6">
12-
<router-view v-slot="{ Component }">
13-
<transition name="page" mode="out-in">
14-
<component :is="Component" />
15-
</transition>
16-
</router-view>
12+
<router-view />
1713
</main>
1814
</div>
1915

@@ -125,20 +121,4 @@ onUnmounted(() => {
125121
})
126122
</script>
127123

128-
<style scoped>
129-
/* Page transitions */
130-
.page-enter-active,
131-
.page-leave-active {
132-
transition: all 0.3s ease;
133-
}
134-
135-
.page-enter-from {
136-
opacity: 0;
137-
transform: translateX(10px);
138-
}
139-
140-
.page-leave-to {
141-
opacity: 0;
142-
transform: translateX(-10px);
143-
}
144-
</style>
124+
<!-- Page transitions removed: caused CSS transition deadlock blocking SPA navigation (QA 2026-03-29) -->

0 commit comments

Comments
 (0)