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/** 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/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