From 8fac1c368e6cad144d296ce17ff272bdd60f4a1c Mon Sep 17 00:00:00 2001 From: Roman Barlos Date: Sun, 5 Oct 2025 10:07:31 +0300 Subject: [PATCH 1/3] fix(logging): capture stderr output before MCP initialization Previously, stderr monitoring started only after successful MCP initialize() handshake. This caused startup errors (like missing API keys or credentials) to be invisible in logs when initialization timed out. Now stderr monitoring begins immediately after process start, before the initialize() call. This ensures all process output is captured and logged, making debugging connection failures much easier. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 814860c78..bc4d41ae2 100644 --- a/.gitignore +++ b/.gitignore @@ -120,4 +120,4 @@ internal_docs/ # Keep cmd/mcpproxy-tray/ directory and its contents (this line is optional) !cmd/mcpproxy-tray/ -!cmd/mcpproxy-tray/** \ No newline at end of file +!cmd/mcpproxy-tray/** From 65ace89ffce30a62ca70881b254883b375dc2dec Mon Sep 17 00:00:00 2001 From: Roman Barlos Date: Sun, 5 Oct 2025 20:18:43 +0300 Subject: [PATCH 2/3] feat(tray): add color icons for Windows (#1) --- internal/tray/managers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/tray/managers.go b/internal/tray/managers.go index 6323f3717..73d5ee38f 100644 --- a/internal/tray/managers.go +++ b/internal/tray/managers.go @@ -677,6 +677,7 @@ func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (dis statusIcon = "⏸️" statusText = "disabled" iconPath = iconPaused +<<<<<<< HEAD } else if st := strings.ToLower(statusValue); st != "" { switch st { case "ready", "connected": @@ -708,6 +709,7 @@ func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (dis statusIcon = "πŸ”΄" statusText = "disconnected" iconPath = iconDisconnected +<<<<<<< HEAD } // On Windows, use icons instead of emoji for better visual appearance From a64e289382df51b917178c8c4c80fee620c4137f Mon Sep 17 00:00:00 2001 From: Roman Barlos Date: Sun, 12 Oct 2025 10:55:55 +0300 Subject: [PATCH 3/3] feat: Add Windows support for mcpproxy-tray - Extend build tags to support Windows (darwin || windows) - Add Windows-specific process monitoring (process_windows.go) - Implement cross-platform OpenWebUI with Windows support - Update binary resolution for .exe files on Windows - Add automatic port conflict resolution --- cmd/mcpproxy-tray/internal/api/adapter.go | 2 +- cmd/mcpproxy-tray/internal/api/client.go | 33 +- cmd/mcpproxy-tray/internal/monitor/health.go | 2 +- .../internal/monitor/process_windows.go | 366 ++++++++++++++++++ cmd/mcpproxy-tray/internal/state/machine.go | 2 +- cmd/mcpproxy-tray/internal/state/states.go | 2 +- cmd/mcpproxy-tray/main.go | 100 ++++- docs/setup.md | 112 ++++-- frontend/.nvmrc | 1 + frontend/package-lock.json | 3 + frontend/package.json | 3 + internal/tray/managers.go | 2 - scripts/build.ps1 | 142 +++++-- 13 files changed, 696 insertions(+), 74 deletions(-) create mode 100644 cmd/mcpproxy-tray/internal/monitor/process_windows.go create mode 100644 frontend/.nvmrc diff --git a/cmd/mcpproxy-tray/internal/api/adapter.go b/cmd/mcpproxy-tray/internal/api/adapter.go index cb6eeb77c..7d538515f 100644 --- a/cmd/mcpproxy-tray/internal/api/adapter.go +++ b/cmd/mcpproxy-tray/internal/api/adapter.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin || windows package api diff --git a/cmd/mcpproxy-tray/internal/api/client.go b/cmd/mcpproxy-tray/internal/api/client.go index f308fb0ce..3ec75d7fe 100644 --- a/cmd/mcpproxy-tray/internal/api/client.go +++ b/cmd/mcpproxy-tray/internal/api/client.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin || windows package api @@ -16,6 +16,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "sort" "strings" "time" @@ -571,14 +572,32 @@ func (c *Client) OpenWebUI() error { c.logger.Info("Opening web control panel", "url", displayURL) } - cmd := exec.Command("open", url) - if err := cmd.Run(); err != nil { - if c.logger != nil { - c.logger.Error("Failed to open web control panel", "url", displayURL, "error", err) + switch runtime.GOOS { + case "darwin": + cmd := exec.Command("open", url) + if err := cmd.Run(); err != nil { + if c.logger != nil { + c.logger.Error("Failed to open web control panel", "url", displayURL, "error", err) + } + return fmt.Errorf("failed to open web control panel: %w", err) } - return fmt.Errorf("failed to open web control panel: %w", err) + return nil + case "windows": + // Try rundll32 first + if err := exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Run(); err == nil { + return nil + } + // Fallback to cmd start + if err := exec.Command("cmd", "/c", "start", "", url).Run(); err != nil { + if c.logger != nil { + c.logger.Error("Failed to open web control panel", "url", displayURL, "error", err) + } + return fmt.Errorf("failed to open web control panel: %w", err) + } + return nil + default: + return fmt.Errorf("unsupported OS for OpenWebUI: %s", runtime.GOOS) } - return nil } // makeRequest makes an HTTP request to the API with enhanced error handling and retry logic diff --git a/cmd/mcpproxy-tray/internal/monitor/health.go b/cmd/mcpproxy-tray/internal/monitor/health.go index 51860eec9..7eb3694b1 100644 --- a/cmd/mcpproxy-tray/internal/monitor/health.go +++ b/cmd/mcpproxy-tray/internal/monitor/health.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin || windows package monitor diff --git a/cmd/mcpproxy-tray/internal/monitor/process_windows.go b/cmd/mcpproxy-tray/internal/monitor/process_windows.go new file mode 100644 index 000000000..b6c066e46 --- /dev/null +++ b/cmd/mcpproxy-tray/internal/monitor/process_windows.go @@ -0,0 +1,366 @@ +//go:build windows + +package monitor + +import ( + "bufio" + "context" + "fmt" + "io" + "os/exec" + "strings" + "sync" + "time" + + "go.uber.org/zap" + + "mcpproxy-go/cmd/mcpproxy-tray/internal/state" +) + +// Windows-specific ProcessMonitor implementation without POSIX process groups. + +// ProcessStatus represents the status of a monitored process +type ProcessStatus string + +const ( + ProcessStatusStopped ProcessStatus = "stopped" + ProcessStatusStarting ProcessStatus = "starting" + ProcessStatusRunning ProcessStatus = "running" + ProcessStatusFailed ProcessStatus = "failed" + ProcessStatusCrashed ProcessStatus = "crashed" +) + +// ProcessEvent represents events from the process monitor +type ProcessEvent struct { + Type ProcessEventType + Data map[string]interface{} + Error error + Timestamp time.Time +} + +type ProcessEventType string + +const ( + ProcessEventStarted ProcessEventType = "started" + ProcessEventExited ProcessEventType = "exited" + ProcessEventError ProcessEventType = "error" + ProcessEventOutput ProcessEventType = "output" +) + +// ExitInfo contains information about process exit +type ExitInfo struct { + Code int + Signal string + Timestamp time.Time + Error error +} + +// ProcessConfig contains configuration for process monitoring +type ProcessConfig struct { + Binary string + Args []string + Env []string + WorkingDir string + StartTimeout time.Duration + CaptureOutput bool +} + +// ProcessMonitor monitors a subprocess and reports its status (Windows) +type ProcessMonitor struct { + config ProcessConfig + logger *zap.SugaredLogger + stateMachine *state.Machine + + mu sync.RWMutex + cmd *exec.Cmd + status ProcessStatus + pid int + exitInfo *ExitInfo + startTime time.Time + + // Channels + eventCh chan ProcessEvent + shutdownCh chan struct{} + + // Output capture + stdoutBuf strings.Builder + stderrBuf strings.Builder + outputMu sync.Mutex + + // Context for cancellation + ctx context.Context + cancel context.CancelFunc +} + +// NewProcessMonitor creates a new process monitor (Windows) +func NewProcessMonitor(config *ProcessConfig, logger *zap.SugaredLogger, stateMachine *state.Machine) *ProcessMonitor { + ctx, cancel := context.WithCancel(context.Background()) + + // Set default timeout if not specified + if config.StartTimeout == 0 { + config.StartTimeout = 30 * time.Second + } + + return &ProcessMonitor{ + config: *config, + logger: logger, + stateMachine: stateMachine, + status: ProcessStatusStopped, + eventCh: make(chan ProcessEvent, 50), + shutdownCh: make(chan struct{}), + ctx: ctx, + cancel: cancel, + } +} + +// Start starts the monitored process (Windows) +func (pm *ProcessMonitor) Start() error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if pm.status == ProcessStatusRunning || pm.status == ProcessStatusStarting { + return fmt.Errorf("process already running or starting") + } + + pm.logger.Info("Starting process", + "binary", pm.config.Binary, + "args", pm.maskSensitiveArgs(pm.config.Args), + "env_count", len(pm.config.Env), + "working_dir", pm.config.WorkingDir) + + // Create command + pm.cmd = exec.CommandContext(pm.ctx, pm.config.Binary, pm.config.Args...) + if pm.config.WorkingDir != "" { + pm.cmd.Dir = pm.config.WorkingDir + } + if len(pm.config.Env) > 0 { + pm.cmd.Env = pm.config.Env + pm.logger.Debug("Process environment", + "env_vars", pm.maskSensitiveEnv(pm.config.Env)) + } + + // Set up output capture if enabled + if pm.config.CaptureOutput { + if err := pm.setupOutputCapture(); err != nil { + return fmt.Errorf("failed to set up output capture: %w", err) + } + } + + pm.status = ProcessStatusStarting + pm.startTime = time.Now() + if err := pm.cmd.Start(); err != nil { + pm.status = ProcessStatusFailed + pm.logger.Error("Failed to start process", "error", err) + return fmt.Errorf("failed to start process: %w", err) + } + + pm.pid = pm.cmd.Process.Pid + pm.status = ProcessStatusRunning + pm.logger.Info("Process started successfully", + "pid", pm.pid, + "startup_time", time.Since(pm.startTime)) + + go pm.monitor() + + pm.sendEvent(ProcessEvent{ + Type: ProcessEventStarted, + Data: map[string]interface{}{ + "pid": pm.pid, + "startup_time": time.Since(pm.startTime), + }, + Timestamp: time.Now(), + }) + + if pm.stateMachine != nil { + pm.stateMachine.SendEvent(state.EventCoreStarted) + } + return nil +} + +// Stop stops the monitored process (Windows) +func (pm *ProcessMonitor) Stop() error { + pm.mu.Lock() + cmd := pm.cmd + pid := pm.pid + pm.mu.Unlock() + + if cmd == nil || cmd.Process == nil { + return fmt.Errorf("no process to stop") + } + + pm.logger.Info("Stopping process", "pid", pid) + + // Try graceful stop via taskkill /T to kill tree + killCmd := exec.Command("taskkill", "/PID", fmt.Sprint(pid), "/T", "/F") + _ = killCmd.Run() + + // Wait for exit with timeout + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + + select { + case err := <-done: + pm.logger.Info("Process stopped", "pid", pid, "error", err) + return err + case <-time.After(10 * time.Second): + // Force kill if still alive + _ = exec.Command("taskkill", "/PID", fmt.Sprint(pid), "/T", "/F").Run() + <-done + return fmt.Errorf("process force killed") + } +} + +// setupOutputCapture sets up stdout/stderr capture (Windows shares POSIX impl) +func (pm *ProcessMonitor) setupOutputCapture() error { + stdoutPipe, err := pm.cmd.StdoutPipe() + if err != nil { return fmt.Errorf("failed to create stdout pipe: %w", err) } + stderrPipe, err := pm.cmd.StderrPipe() + if err != nil { stdoutPipe.Close(); return fmt.Errorf("failed to create stderr pipe: %w", err) } + go pm.captureOutput(stdoutPipe, &pm.stdoutBuf, "stdout") + go pm.captureOutput(stderrPipe, &pm.stderrBuf, "stderr") + return nil +} + +// captureOutput mirrors POSIX version +func (pm *ProcessMonitor) captureOutput(pipe io.ReadCloser, buf *strings.Builder, streamName string) { + defer pipe.Close() + scanner := bufio.NewScanner(pipe) + for scanner.Scan() { + line := scanner.Text() + pm.outputMu.Lock() + buf.WriteString(line) + buf.WriteString("\n") + pm.outputMu.Unlock() + select { + case pm.eventCh <- ProcessEvent{Type: ProcessEventOutput, Data: map[string]interface{}{"stream": streamName, "line": line}, Timestamp: time.Now()}: + default: + } + lower := strings.ToLower(line) + if strings.Contains(lower, "failed to bind") || strings.Contains(lower, "address already in use") || strings.Contains(lower, "only one usage of each socket") || (strings.Contains(lower, "bind:") && strings.Contains(lower, "forbidden by its access permissions")) { + // Proactively notify about port conflict + if pm.stateMachine != nil { + pm.stateMachine.SendEvent(state.EventPortConflict) + } + } + + if strings.Contains(lower, "error") || strings.Contains(lower, "failed") || strings.Contains(lower, "panic") { + pm.logger.Warn("Process error output", "stream", streamName, "line", line) + } else { + pm.logger.Debug("Process output", "stream", streamName, "line", line) + } + } + if err := scanner.Err(); err != nil { + pm.logger.Warn("Error reading process output", "stream", streamName, "error", err) + } +} + +// monitor waits for process exit (Windows) +func (pm *ProcessMonitor) monitor() { + defer close(pm.eventCh) + err := pm.cmd.Wait() + pm.mu.Lock() + pm.exitInfo = &ExitInfo{Timestamp: time.Now(), Error: err} + if err != nil { + pm.status = ProcessStatusFailed + // Extract exit code on Windows + if exitErr, ok := err.(*exec.ExitError); ok { + code := exitErr.ExitCode() + pm.exitInfo.Code = code + pm.logger.Error("Process exited with error", "pid", pm.pid, "error", err, "exit_code", code, "runtime", time.Since(pm.startTime)) + } else { + pm.logger.Error("Process exited with error", "pid", pm.pid, "error", err, "runtime", time.Since(pm.startTime)) + } + } else { + pm.status = ProcessStatusStopped + pm.exitInfo.Code = 0 + pm.logger.Info("Process exited normally", "pid", pm.pid, "runtime", time.Since(pm.startTime)) + } + exitInfo := *pm.exitInfo + pm.mu.Unlock() + pm.sendEvent(ProcessEvent{Type: ProcessEventExited, Data: map[string]interface{}{"exit_code": exitInfo.Code, "runtime": time.Since(pm.startTime)}, Error: exitInfo.Error, Timestamp: exitInfo.Timestamp}) + if pm.stateMachine != nil { + if exitInfo.Error == nil { + return + } + // Map Windows exit codes similar to POSIX implementation + switch exitInfo.Code { + case 2: + pm.stateMachine.SendEvent(state.EventPortConflict) + case 3: + pm.stateMachine.SendEvent(state.EventDBLocked) + case 4: + pm.stateMachine.SendEvent(state.EventConfigError) + default: + pm.stateMachine.SendEvent(state.EventGeneralError) + } + } +} + +// Shutdown gracefully shuts down the monitor +func (pm *ProcessMonitor) Shutdown() { + pm.logger.Info("Process monitor shutting down") + pm.cancel() + if pm.GetStatus() == ProcessStatusRunning { + _ = pm.Stop() + } + close(pm.shutdownCh) +} + +// GetStatus returns the current process status (Windows) +func (pm *ProcessMonitor) GetStatus() ProcessStatus { + pm.mu.RLock() + defer pm.mu.RUnlock() + return pm.status +} + +// sendEvent sends an event to the channel (Windows) +func (pm *ProcessMonitor) sendEvent(event ProcessEvent) { + select { + case pm.eventCh <- event: + default: + pm.logger.Warn("Process event channel full, dropping event", "event_type", event.Type) + } +} + +// maskSensitiveArgs masks sensitive data in command arguments (Windows) +func (pm *ProcessMonitor) maskSensitiveArgs(args []string) []string { + masked := make([]string, len(args)) + copy(masked, args) + for i, arg := range masked { + low := strings.ToLower(arg) + if strings.Contains(low, "key") || strings.Contains(low, "secret") || strings.Contains(low, "token") || strings.Contains(low, "password") { + if len(arg) > 8 { + masked[i] = arg[:4] + "****" + arg[len(arg)-4:] + } else { + masked[i] = "****" + } + } + } + return masked +} + +// maskSensitiveEnv masks sensitive data in environment variables (Windows) +func (pm *ProcessMonitor) maskSensitiveEnv(env []string) []string { + masked := make([]string, len(env)) + for i, envVar := range env { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) != 2 { + masked[i] = envVar + continue + } + keyLower := strings.ToLower(parts[0]) + value := parts[1] + if strings.Contains(keyLower, "key") || strings.Contains(keyLower, "secret") || strings.Contains(keyLower, "token") || strings.Contains(keyLower, "password") { + if len(value) > 8 { + masked[i] = parts[0] + "=" + value[:4] + "****" + value[len(value)-4:] + } else { + masked[i] = parts[0] + "=****" + } + } else { + masked[i] = envVar + } + } + return masked +} + + diff --git a/cmd/mcpproxy-tray/internal/state/machine.go b/cmd/mcpproxy-tray/internal/state/machine.go index 996a1a7bd..c4145a123 100644 --- a/cmd/mcpproxy-tray/internal/state/machine.go +++ b/cmd/mcpproxy-tray/internal/state/machine.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin || windows package state diff --git a/cmd/mcpproxy-tray/internal/state/states.go b/cmd/mcpproxy-tray/internal/state/states.go index 773baf172..af98b5fe1 100644 --- a/cmd/mcpproxy-tray/internal/state/states.go +++ b/cmd/mcpproxy-tray/internal/state/states.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin || windows package state diff --git a/cmd/mcpproxy-tray/main.go b/cmd/mcpproxy-tray/main.go index de55aad24..fda556472 100644 --- a/cmd/mcpproxy-tray/main.go +++ b/cmd/mcpproxy-tray/main.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin || windows package main @@ -446,8 +446,11 @@ func findMcpproxyBinary() (string, error) { } } - // 2. Working-directory relative binary (local dev workflow). - addCandidate(filepath.Join(".", "mcpproxy")) + // 2. Working-directory relative binary (local dev workflow). + addCandidate(filepath.Join(".", "mcpproxy")) + if runtime.GOOS == platformWindows { + addCandidate(filepath.Join(".", "mcpproxy-windows-amd64")) + } // 3. Managed installation directories (Application Support on macOS). if homeDir, err := os.UserHomeDir(); err == nil { @@ -457,7 +460,7 @@ func findMcpproxyBinary() (string, error) { } } - // 4. Common package manager locations. + // 4. Common package manager locations. addCandidate("/opt/homebrew/bin/mcpproxy") addCandidate("/usr/local/bin/mcpproxy") @@ -477,6 +480,36 @@ func findMcpproxyBinary() (string, error) { func resolveExecutableCandidate(path string) (string, bool) { var abs string + if runtime.GOOS == platformWindows { + candidate := path + lower := strings.ToLower(candidate) + // try adding .exe if not present + if !strings.HasSuffix(lower, ".exe") { + if filepath.IsAbs(candidate) { + candidate = candidate + ".exe" + } else { + candidate = candidate + ".exe" + } + } + + if filepath.IsAbs(candidate) { + abs = candidate + } else { + var err error + abs, err = filepath.Abs(candidate) + if err != nil { + return "", false + } + } + + info, err := os.Stat(abs) + if err != nil || info.IsDir() { + return "", false + } + // On Windows, execute bit is not meaningful; presence is enough + return abs, true + } + if filepath.IsAbs(path) { abs = path } else { @@ -946,8 +979,63 @@ func (cpl *CoreProcessLauncher) handleReconnecting(_ context.Context) { // handlePortConflictError handles port conflict errors func (cpl *CoreProcessLauncher) handlePortConflictError() { - cpl.logger.Warn("Core failed due to port conflict") - // Could implement automatic port resolution here + cpl.logger.Warn("Core failed due to port conflict") + // Attempt automatic port resolution on Windows/macOS + // 1) Parse current coreURL and extract port + u, err := url.Parse(cpl.coreURL) + if err != nil { + cpl.logger.Error("Failed to parse coreURL for port conflict handling", "core_url", cpl.coreURL, "error", err) + return + } + portStr := u.Port() + if portStr == "" { + portStr = "8080" + } + baseHost := u.Hostname() + if baseHost == "" { + baseHost = "127.0.0.1" + } + // 2) Find next available port + startPort, _ := strconv.Atoi(portStr) + newPort, err := findNextAvailablePort(startPort + 1, startPort+50) + if err != nil { + cpl.logger.Error("Failed to find available port after conflict", "start_port", startPort, "error", err) + return + } + // 3) Update coreURL and restart flow + u.Host = net.JoinHostPort(baseHost, strconv.Itoa(newPort)) + cpl.coreURL = u.String() + cpl.logger.Info("Auto-selected alternate port after conflict", "new_core_url", cpl.coreURL) + + // Stop monitors so they can be recreated with new URL + if cpl.healthMonitor != nil { + cpl.healthMonitor.Stop() + cpl.healthMonitor = nil + } + if cpl.processMonitor != nil { + cpl.processMonitor.Shutdown() + cpl.processMonitor = nil + } + // Trigger retry which will launch core with updated args based on coreURL + cpl.stateMachine.SendEvent(state.EventRetry) +} + +// findNextAvailablePort scans a range and returns the first free port on localhost +func findNextAvailablePort(start, end int) (int, error) { + if start < 1 { + start = 1 + } + if end <= start { + end = start + 50 + } + for p := start; p <= end; p++ { + ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(p))) + if err == nil { + _ = ln.Close() + return p, nil + } + } + return 0, fmt.Errorf("no free port in range %d-%d", start, end) } // handleDBLockedError handles database locked errors diff --git a/docs/setup.md b/docs/setup.md index 90432d5de..4a31f18f5 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -7,6 +7,7 @@ A comprehensive guide to connect mcpproxy (http-streamable) to popular MCP clien MCPProxy is a smart Model Context Protocol (MCP) proxy that provides intelligent tool discovery and proxying for MCP servers. It runs as an HTTP server that aggregates multiple upstream MCP servers into a single endpoint, making it easy to connect multiple AI tools and services to your favorite IDE or AI assistant. **Key Features:** + - **HTTP Streamable**: Uses MCP's streamable HTTP transport for efficient communication - **Smart Tool Discovery**: Automatically indexes and searches tools from multiple upstream servers - **Unified Interface**: Single endpoint for multiple MCP servers @@ -21,11 +22,13 @@ MCPProxy is a smart Model Context Protocol (MCP) proxy that provides intelligent Download the DMG installer from [GitHub Releases](https://github.com/smart-mcp-proxy/mcpproxy-go/releases) for the easiest installation experience. **macOS (Homebrew):** + ```bash brew install smart-mcp-proxy/mcpproxy/mcpproxy ``` **Go Install:** + ```bash go install github.com/smart-mcp-proxy/mcpproxy-go/cmd/mcpproxy@latest ``` @@ -33,6 +36,7 @@ go install github.com/smart-mcp-proxy/mcpproxy-go/cmd/mcpproxy@latest ### 2. Run MCPProxy **From Terminal:** + ```bash mcpproxy serve ``` @@ -42,6 +46,7 @@ mcpproxy serve This starts MCPProxy on the default port `:8080` with HTTP endpoint at `http://localhost:8080/mcp/` **πŸ“ Note:** + - MCPProxy starts with **HTTP by default** for immediate compatibility - HTTPS is available optionally for enhanced security (see [HTTPS Setup](#optional-https-setup) below) - At first launch, MCPProxy will automatically generate a minimal configuration file if none exists @@ -51,6 +56,7 @@ This starts MCPProxy on the default port `:8080` with HTTP endpoint at `http://l **Check if port 8080 is already in use:** **macOS/Linux:** + ```bash lsof -i :8080 # or @@ -58,11 +64,13 @@ netstat -an | grep 8080 ``` **Windows:** + ```bash netstat -an | findstr 8080 ``` **Change Default Port:** + ```bash mcpproxy serve --listen :8081 # or set in config file @@ -72,13 +80,14 @@ mcpproxy serve --listen :8081 MCPProxy looks for configuration in these locations (in order): -| OS | Config Location | -|---|---| -| **macOS** | `~/.mcpproxy/mcp_config.json` | +| OS | Config Location | +| ----------- | ----------------------------------------- | +| **macOS** | `~/.mcpproxy/mcp_config.json` | | **Windows** | `%USERPROFILE%\.mcpproxy\mcp_config.json` | -| **Linux** | `~/.mcpproxy/mcp_config.json` | +| **Linux** | `~/.mcpproxy/mcp_config.json` | **Sample Configuration:** + ```jsonc { "listen": ":8080", @@ -91,8 +100,19 @@ MCPProxy looks for configuration in these locations (in order): "tool_response_limit": 20000, "mcpServers": [ - { "name": "local-python", "command": "python", "args": ["-m", "my_server"], "type": "stdio", "enabled": true }, - { "name": "remote-http", "url": "http://localhost:3001", "type": "http", "enabled": true } + { + "name": "local-python", + "command": "python", + "args": ["-m", "my_server"], + "type": "stdio", + "enabled": true + }, + { + "name": "remote-http", + "url": "http://localhost:3001", + "type": "http", + "enabled": true + } ] } ``` @@ -104,10 +124,12 @@ MCPProxy looks for configuration in these locations (in order): ### 🎯 Cursor IDE **Method 1: One-Click Install** + 1. Visit: https://mcpproxy.app/cursor-install.html 2. Click "Install in Cursor IDE" **Method 2: Manual Setup** + 1. Open Cursor Settings (`Cmd/Ctrl + ,`) 2. Click "Tools & Integrations" 3. Add MCP Server with this configuration: @@ -122,6 +144,7 @@ MCPProxy looks for configuration in these locations (in order): ``` **Verification:** + - **Option 1:** Restart Cursor completely - **Option 2:** Disable and re-enable the MCP server in Cursor Settings > Tools & Integrations - **⚠️ Important:** Make sure MCPProxy is running (check for tray icon if enabled) @@ -134,6 +157,7 @@ MCPProxy looks for configuration in these locations (in order): VS Code has built-in MCP support starting from version 1.102. **Setup Steps:** + 1. Install **GitHub Copilot** and **Copilot Chat** extensions 2. Open VS Code Settings (`Cmd/Ctrl + ,`) 3. Search for "mcp" in settings @@ -161,7 +185,7 @@ Create `.vscode/mcp.json` in your workspace: { "servers": { "MCPProxy": { - "type": "http", + "type": "http", "url": "http://localhost:8080/mcp/" } } @@ -169,8 +193,9 @@ Create `.vscode/mcp.json` in your workspace: ``` **Usage:** + 1. Open Copilot Chat -2. Select **Agent Mode** +2. Select **Agent Mode** 3. Click Tools icon to see available tools 4. MCPProxy tools will be listed @@ -192,11 +217,11 @@ Add mcpproxy as a remote MCP server via Settings β†’ Connectors β†’ Add Custom C **Configuration Paths:** -| OS | Claude Desktop Config Path | -|---|---| -| **macOS** | `~/Library/Application Support/Claude/claude_desktop_config.json` | -| **Windows** | `%APPDATA%\Claude\claude_desktop_config.json` | -| **Linux** | `~/.config/Claude/claude_desktop_config.json` | +| OS | Claude Desktop Config Path | +| ----------- | ----------------------------------------------------------------- | +| **macOS** | `~/Library/Application Support/Claude/claude_desktop_config.json` | +| **Windows** | `%APPDATA%\Claude\claude_desktop_config.json` | +| **Linux** | `~/.config/Claude/claude_desktop_config.json` | **Setup Steps:** @@ -205,18 +230,21 @@ Add mcpproxy as a remote MCP server via Settings β†’ Connectors β†’ Add Custom C 1. Create the config file if it doesn't exist: **macOS:** + ```bash mkdir -p ~/Library/Application\ Support/Claude/ touch ~/Library/Application\ Support/Claude/claude_desktop_config.json ``` **Windows:** + ```bash mkdir "%APPDATA%\Claude" type nul > "%APPDATA%\Claude\claude_desktop_config.json" ``` **Linux:** + ```bash mkdir -p ~/.config/Claude/ touch ~/.config/Claude/claude_desktop_config.json @@ -259,16 +287,18 @@ touch ~/.config/Claude/claude_desktop_config.json Goose is a command-line AI agent that supports MCP servers through its extension system. **Prerequisites:** + - Python 3.8+ or Go 1.19+ - Goose installed: https://github.com/block/goose **Setup via CLI:** + ```bash # Configure Goose goose configure # Choose: Add Extension -# Choose: Remote Extension +# Choose: Remote Extension # Name: MCPProxy # URL: http://localhost:8080/mcp/ # Timeout: 300 (default) @@ -286,6 +316,7 @@ extensions: ``` **Usage:** + ```bash # Start Goose session goose @@ -315,11 +346,14 @@ MCPProxy supports secure HTTPS connections with automatic certificate generation ### Quick HTTPS Setup **Step 1: Install Certificate (One-time setup)** + ```bash # Trust the mcpproxy CA certificate mcpproxy trust-cert ``` + This command will: + - Generate a local CA certificate if needed - Install it to your system's trusted certificate store - Prompt for your password once (required for keychain access) @@ -329,6 +363,7 @@ This command will: Choose one of these methods: **Option A: Environment Variable (Temporary)** + ```bash export MCPPROXY_TLS_ENABLED=true mcpproxy serve @@ -336,6 +371,7 @@ mcpproxy serve **Option B: Configuration File (Permanent)** Edit `~/.mcpproxy/mcp_config.json`: + ```json { "listen": ":8080", @@ -352,6 +388,7 @@ Edit `~/.mcpproxy/mcp_config.json`: After enabling HTTPS, update your client configurations to use `https://` URLs: **Cursor IDE:** + ```json { "MCPProxy": { @@ -362,6 +399,7 @@ After enabling HTTPS, update your client configurations to use `https://` URLs: ``` **VS Code:** + ```json { "mcp": { @@ -376,6 +414,7 @@ After enabling HTTPS, update your client configurations to use `https://` URLs: ``` **Claude Desktop (with certificate trust):** + ```json { "mcpServers": { @@ -393,6 +432,7 @@ After enabling HTTPS, update your client configurations to use `https://` URLs: ### HTTPS Configuration Options **Basic HTTPS (Recommended):** + ```json { "tls": { @@ -402,6 +442,7 @@ After enabling HTTPS, update your client configurations to use `https://` URLs: ``` **Advanced HTTPS with mTLS:** + ```json { "tls": { @@ -414,6 +455,7 @@ After enabling HTTPS, update your client configurations to use `https://` URLs: ``` **Configuration Options:** + - `enabled`: Enable/disable HTTPS (default: `false`) - `require_client_cert`: Enable mutual TLS (mTLS) for client authentication - `certs_dir`: Custom directory for certificates (default: `{data_dir}/certs`) @@ -422,11 +464,13 @@ After enabling HTTPS, update your client configurations to use `https://` URLs: ### Certificate Management **Certificate Locations:** + - **CA Certificate**: `~/.mcpproxy/certs/ca.pem` - **Server Certificate**: `~/.mcpproxy/certs/localhost.pem` - **Private Keys**: `~/.mcpproxy/certs/*.key` (automatically secured) **View Certificate Details:** + ```bash # View CA certificate info openssl x509 -in ~/.mcpproxy/certs/ca.pem -text -noout @@ -436,6 +480,7 @@ openssl verify -CAfile ~/.mcpproxy/certs/ca.pem ~/.mcpproxy/certs/localhost.pem ``` **Regenerate Certificates:** + ```bash # Remove existing certificates rm -rf ~/.mcpproxy/certs @@ -452,6 +497,7 @@ mcpproxy trust-cert **Certificate Trust Issues:** If you get SSL/TLS errors, verify certificate trust: + ```bash # Test certificate trust curl -f https://localhost:8080/health @@ -463,6 +509,7 @@ mcpproxy trust-cert --force **Claude Desktop Certificate Issues:** If Claude Desktop shows certificate errors: + 1. Ensure `NODE_EXTRA_CA_CERTS` points to the correct certificate path 2. Use absolute path: `/Users/yourusername/.mcpproxy/certs/ca.pem` 3. Restart Claude Desktop after configuration changes @@ -470,6 +517,7 @@ If Claude Desktop shows certificate errors: **Browser Certificate Warnings:** When accessing the Web UI at `https://localhost:8080/ui/`: + 1. Click "Advanced" on the certificate warning 2. Click "Proceed to localhost (unsafe)" 3. This is expected for self-signed certificates @@ -488,12 +536,13 @@ When accessing the Web UI at `https://localhost:8080/ui/`: ### Check Current Port Usage **Find MCPProxy Process:** + ```bash # macOS/Linux ps aux | grep mcpproxy lsof -i :8080 -# Windows +# Windows tasklist | findstr mcpproxy netstat -ano | findstr :8080 ``` @@ -501,6 +550,7 @@ netstat -ano | findstr :8080 ### Change Default Port **Command Line:** + ```bash mcpproxy serve --listen :8081 mcpproxy serve --listen :9000 @@ -508,14 +558,16 @@ mcpproxy serve --listen 127.0.0.1:8080 # Bind to specific interface ``` **Configuration File:** + ```json { - "listen": ":8081", + "listen": ":8081" // ... rest of config } ``` **Environment Variable:** + ```bash export MCPPROXY_LISTEN=":8081" mcpproxy serve @@ -528,7 +580,7 @@ mcpproxy serve Run multiple MCPProxy instances on different ports: ```bash -# Instance 1 - Development +# Instance 1 - Development mcpproxy serve --config dev_config.json --listen :8080 # Instance 2 - Production @@ -540,6 +592,7 @@ mcpproxy serve --config prod_config.json --listen :8081 ### Common Issues **1. Port Already in Use** + ```bash # Kill process using port 8080 lsof -ti:8080 | xargs kill -9 # macOS/Linux @@ -548,6 +601,7 @@ taskkill /PID /F # Windows ``` **2. MCPProxy Not Starting** + ```bash # Check logs mcpproxy serve --log-level debug @@ -557,6 +611,7 @@ mcpproxy serve --config ~/.mcpproxy/mcp_config.json --log-level debug ``` **3. Client Connection Issues** + - Verify MCPProxy is running: Check process with `ps aux | grep mcpproxy` - Check firewall settings - Ensure correct URL in client config @@ -564,6 +619,7 @@ mcpproxy serve --config ~/.mcpproxy/mcp_config.json --log-level debug - Check tray icon (if enabled) for status **4. Tools Not Appearing** + - Check MCPProxy upstream server configuration - Verify upstream servers are running - Check MCPProxy logs for errors @@ -571,6 +627,7 @@ mcpproxy serve --config ~/.mcpproxy/mcp_config.json --log-level debug - Use `mcpproxy tools list --server=SERVER_NAME` to test individual servers **5. Server Connection Problems** + - Test individual servers: `mcpproxy tools list --server=SERVER_NAME --log-level=trace` - Check authentication: Look for OAuth URLs in console output - Verify server configuration: Ensure URL, command, and protocol are correct @@ -579,6 +636,7 @@ mcpproxy serve --config ~/.mcpproxy/mcp_config.json --log-level debug ### Debug Commands **Test MCPProxy Status:** + ```bash # Check if MCPProxy is running ps aux | grep mcpproxy @@ -591,6 +649,7 @@ mcpproxy serve --log-level debug ``` **Debug Individual Servers:** + ```bash # List tools from a specific server with detailed debugging mcpproxy tools list --server=github-server --log-level=trace @@ -607,6 +666,7 @@ mcpproxy tools list --server=weather-api --output=json **πŸ“ Note:** MCPProxy uses the MCP protocol over HTTP, not simple REST endpoints. Use MCP clients to interact with the server, not direct curl commands. **View Logs:** + ```bash # Real-time logs (macOS/Linux) tail -f ~/Library/Logs/mcpproxy/main.log @@ -624,11 +684,11 @@ tail -f ~/Library/Logs/mcpproxy/main.log | grep -E "(github-server|oauth|error)" ```json { - "listen": "127.0.0.1:8080", // Bind to localhost only - "read_only_mode": true, // Prevent configuration changes - "disable_management": true, // Disable server management tools - "allow_server_add": false, // Prevent adding new servers - "allow_server_remove": false // Prevent removing servers + "listen": "127.0.0.1:8080", // Bind to localhost only + "read_only_mode": true, // Prevent configuration changes + "disable_management": true, // Disable server management tools + "allow_server_add": false, // Prevent adding new servers + "allow_server_remove": false // Prevent removing servers } ``` @@ -636,9 +696,9 @@ tail -f ~/Library/Logs/mcpproxy/main.log | grep -E "(github-server|oauth|error)" ```json { - "top_k": 10, // More search results - "tools_limit": 25, // More tools per request - "tool_response_limit": 50000 // Larger response limit + "top_k": 10, // More search results + "tools_limit": 25, // More tools per request + "tool_response_limit": 50000 // Larger response limit } ``` @@ -679,4 +739,4 @@ For servers requiring authentication: --- -*Need help? Join our community or open an issue on GitHub.* \ No newline at end of file +_Need help? Join our community or open an issue on GitHub._ diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000..3c032078a --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0c731d498..32270c1e2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -38,6 +38,9 @@ "vite": "^5.4.20", "vitest": "^2.0.0", "vue-tsc": "^2.2.12" + }, + "engines": { + "node": ">=18.18.0" } }, "node_modules/@alloc/quick-lru": { diff --git a/frontend/package.json b/frontend/package.json index aa5e719d8..4b149be65 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "MCPProxy Web Control Panel - Vue 3 + TypeScript + DaisyUI", "type": "module", + "engines": { + "node": ">=18.18.0" + }, "scripts": { "dev": "vite", "build": "vue-tsc && vite build", diff --git a/internal/tray/managers.go b/internal/tray/managers.go index 73d5ee38f..6323f3717 100644 --- a/internal/tray/managers.go +++ b/internal/tray/managers.go @@ -677,7 +677,6 @@ func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (dis statusIcon = "⏸️" statusText = "disabled" iconPath = iconPaused -<<<<<<< HEAD } else if st := strings.ToLower(statusValue); st != "" { switch st { case "ready", "connected": @@ -709,7 +708,6 @@ func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (dis statusIcon = "πŸ”΄" statusText = "disconnected" iconPath = iconDisconnected -<<<<<<< HEAD } // On Windows, use icons instead of emoji for better visual appearance diff --git a/scripts/build.ps1 b/scripts/build.ps1 index d41f4cc9b..45dfe4a38 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -2,7 +2,9 @@ # PowerShell build script for mcpproxy param( - [string]$Version = "" + [string]$Version = "", + [switch]$BuildTray, + [switch]$OnlyCurrentPlatform ) # Enable strict mode @@ -42,6 +44,9 @@ Write-Host "" $LDFLAGS = "-X main.version=$Version -X main.commit=$Commit -X main.date=$Date -s -w" +# Array to track built binaries +$BuiltBinaries = @() + # Build for current platform (with CGO for tray support if needed) Write-Host "Building for current platform..." -ForegroundColor Cyan go build -ldflags $LDFLAGS -o mcpproxy.exe ./cmd/mcpproxy @@ -49,41 +54,120 @@ if ($LASTEXITCODE -ne 0) { Write-Error "Failed to build for current platform" exit $LASTEXITCODE } +$BuiltBinaries += "mcpproxy.exe" -# Build for Linux (with CGO disabled to avoid systray issues) -Write-Host "Building for Linux..." -ForegroundColor Cyan -$env:CGO_ENABLED = "0" -$env:GOOS = "linux" -$env:GOARCH = "amd64" -go build -ldflags $LDFLAGS -o mcpproxy-linux-amd64 ./cmd/mcpproxy -if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to build for Linux" - exit $LASTEXITCODE -} +if (!$OnlyCurrentPlatform) { + # Build for Linux (with CGO disabled to avoid systray issues) + Write-Host "Building for Linux..." -ForegroundColor Cyan + $env:CGO_ENABLED = "0" + $env:GOOS = "linux" + $env:GOARCH = "amd64" + go build -ldflags $LDFLAGS -o mcpproxy-linux-amd64 ./cmd/mcpproxy + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to build for Linux" + exit $LASTEXITCODE + } + $BuiltBinaries += "mcpproxy-linux-amd64" -# Build for Windows (with CGO disabled to avoid systray issues) -Write-Host "Building for Windows..." -ForegroundColor Cyan -$env:CGO_ENABLED = "0" -$env:GOOS = "windows" -$env:GOARCH = "amd64" -go build -ldflags $LDFLAGS -o mcpproxy-windows-amd64.exe ./cmd/mcpproxy -if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to build for Windows" - exit $LASTEXITCODE -} + # Build for Windows (with CGO disabled to avoid systray issues) + Write-Host "Building for Windows..." -ForegroundColor Cyan + $env:CGO_ENABLED = "0" + $env:GOOS = "windows" + $env:GOARCH = "amd64" + go build -ldflags $LDFLAGS -o mcpproxy-windows-amd64.exe ./cmd/mcpproxy + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to build for Windows" + exit $LASTEXITCODE + } + $BuiltBinaries += "mcpproxy-windows-amd64.exe" -# Reset environment variables -Remove-Item Env:CGO_ENABLED -ErrorAction SilentlyContinue -Remove-Item Env:GOOS -ErrorAction SilentlyContinue -Remove-Item Env:GOARCH -ErrorAction SilentlyContinue + # Reset environment variables + Remove-Item Env:CGO_ENABLED -ErrorAction SilentlyContinue + Remove-Item Env:GOOS -ErrorAction SilentlyContinue + Remove-Item Env:GOARCH -ErrorAction SilentlyContinue + + # Build for macOS (skip on Windows as cross-compilation for macOS systray is problematic) + Write-Host "Skipping macOS builds (running on Windows - systray dependencies prevent cross-compilation)" -ForegroundColor Yellow +} else { + Write-Host "Skipping cross-platform builds (OnlyCurrentPlatform flag is set)" -ForegroundColor Yellow +} -# Build for macOS (skip on Windows as cross-compilation for macOS systray is problematic) -Write-Host "Skipping macOS builds (running on Windows - systray dependencies prevent cross-compilation)" -ForegroundColor Yellow +# Build tray binaries if requested +if ($BuildTray) { + Write-Host "" + Write-Host "Building mcpproxy-tray binaries..." -ForegroundColor Magenta + + # Build tray for current platform (Windows with CGO) + Write-Host "Building tray for Windows..." -ForegroundColor Cyan + $env:CGO_ENABLED = "1" + $env:GOOS = "windows" + $env:GOARCH = "amd64" + go build -ldflags $LDFLAGS -o mcpproxy-tray.exe ./cmd/mcpproxy-tray + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to build tray for Windows" + } else { + Write-Host "βœ“ Windows tray binary built successfully" -ForegroundColor Green + $BuiltBinaries += "mcpproxy-tray.exe" + } + + if (!$OnlyCurrentPlatform) { + # Build tray for macOS (requires macOS host for proper CGO compilation) + Write-Host "Attempting to build tray for macOS..." -ForegroundColor Cyan + $env:CGO_ENABLED = "1" + $env:GOOS = "darwin" + $env:GOARCH = "amd64" + go build -ldflags $LDFLAGS -o mcpproxy-tray-darwin-amd64 ./cmd/mcpproxy-tray + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to build tray for macOS (cross-compilation from Windows requires proper CGO toolchain)" + } else { + Write-Host "βœ“ macOS tray binary built successfully" -ForegroundColor Green + $BuiltBinaries += "mcpproxy-tray-darwin-amd64" + } + + # Build tray for macOS ARM64 + Write-Host "Attempting to build tray for macOS ARM64..." -ForegroundColor Cyan + $env:CGO_ENABLED = "1" + $env:GOOS = "darwin" + $env:GOARCH = "arm64" + go build -ldflags $LDFLAGS -o mcpproxy-tray-darwin-arm64 ./cmd/mcpproxy-tray + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to build tray for macOS ARM64 (cross-compilation from Windows requires proper CGO toolchain)" + } else { + Write-Host "βœ“ macOS ARM64 tray binary built successfully" -ForegroundColor Green + $BuiltBinaries += "mcpproxy-tray-darwin-arm64" + } + } else { + Write-Host "Skipping cross-platform tray builds (OnlyCurrentPlatform flag is set)" -ForegroundColor Yellow + } + + # Reset environment variables + Remove-Item Env:CGO_ENABLED -ErrorAction SilentlyContinue + Remove-Item Env:GOOS -ErrorAction SilentlyContinue + Remove-Item Env:GOARCH -ErrorAction SilentlyContinue +} Write-Host "" Write-Host "Build complete!" -ForegroundColor Green -Write-Host "Available binaries:" -ForegroundColor Green -Get-ChildItem -Path . -Filter "mcpproxy*" | Select-Object Name, Length, LastWriteTime | Format-Table -AutoSize +Write-Host "Built binaries:" -ForegroundColor Green + +# Display only the binaries built in this run +$BuiltFiles = @() +foreach ($binary in $BuiltBinaries) { + if (Test-Path $binary) { + $fileInfo = Get-Item $binary + $BuiltFiles += [PSCustomObject]@{ + Name = $fileInfo.Name + 'Size (MB)' = [math]::Round($fileInfo.Length / 1MB, 2) + LastWriteTime = $fileInfo.LastWriteTime + } + } +} + +if ($BuiltFiles.Count -gt 0) { + $BuiltFiles | Format-Table -AutoSize +} else { + Write-Host " No binaries were built" -ForegroundColor Yellow +} Write-Host "" Write-Host "Test version info:" -ForegroundColor Cyan