diff --git a/cmd/mcpproxy-tray/main.go b/cmd/mcpproxy-tray/main.go index 7cf2428e..0c90361f 100644 --- a/cmd/mcpproxy-tray/main.go +++ b/cmd/mcpproxy-tray/main.go @@ -47,6 +47,7 @@ var ( defaultCoreURL = "http://127.0.0.1:8080" errNoBundledCore = errors.New("no bundled core binary found") trayAPIKey = "" // API key generated for core communication + trayCLIListen = "" // --listen from the tray's own command line (set in main) shutdownComplete = make(chan struct{}) // Signal when shutdown is complete shutdownOnce sync.Once ) @@ -101,6 +102,11 @@ func main() { logger.Info("Starting mcpproxy-tray", zap.String("version", version)) + // Pick up a --listen flag from the tray's own command line (launchers + // commonly invoke `mcpproxy-tray serve --listen `, mirroring the + // core's serve command). It must be forwarded to the spawned core. + trayCLIListen = trayListenFromArgs(os.Args[1:]) + // Check environment variables for configuration coreTimeout := getCoreTimeout() retryDelay := getRetryDelay() @@ -114,7 +120,8 @@ func main() { zap.Duration("core_timeout", coreTimeout), zap.Duration("retry_delay", retryDelay), zap.Bool("state_debug", stateDebug), - zap.Bool("skip_core", shouldSkipCoreLaunch())) + zap.Bool("skip_core", shouldSkipCoreLaunch()), + zap.String("cli_listen", trayCLIListen)) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -824,9 +831,17 @@ func buildCoreArgs(coreURL string) []string { args = append(args, "--config", cfg) } - // IMPORTANT: Only add --listen for TCP/HTTP connections - // Socket/pipe connections should NOT have --listen (core enables socket by default) - if !isSocketEndpoint(coreURL) { + // An explicit --listen on the tray's own command line is ALWAYS forwarded + // to the core, even when the tray talks to the core over a socket/pipe: + // the socket only covers tray<->core communication, while the core must + // still open the advertised TCP address (otherwise it silently falls back + // to the config-file default, typically 127.0.0.1:8080). + if cliListen := normalizeListen(strings.TrimSpace(trayCLIListen)); cliListen != "" { + args = append(args, "--listen", cliListen) + } else if !isSocketEndpoint(coreURL) { + // IMPORTANT: Only derive --listen from the core URL / env for TCP/HTTP + // connections. Socket/pipe connections should NOT get a derived + // --listen (core enables the socket by default). if listen := listenArgFromURL(coreURL); listen != "" { args = append(args, "--listen", listen) } else if listenEnv := normalizeListen(strings.TrimSpace(os.Getenv("MCPPROXY_TRAY_LISTEN"))); listenEnv != "" { @@ -909,6 +924,40 @@ func shellQuote(arg string) string { return builder.String() } +// trayListenFromArgs extracts the value of a --listen / -l flag from the +// tray's command-line arguments. The tray historically ignored its CLI +// entirely, so launchers invoking `mcpproxy-tray serve --listen ` +// (mirroring the core's serve command) had the listen address silently +// dropped and the core fell back to the config-file default. Any other +// arguments remain ignored. Malformed occurrences — a dangling flag, an +// empty value, or a value that is itself another flag (starts with "-") — +// are skipped and scanning continues, so the first valid listen value wins. +// Returns "" when no valid listen value is present. +func trayListenFromArgs(args []string) string { + for i := 0; i < len(args); i++ { + arg := args[i] + var value string + switch { + case arg == "--listen" || arg == "-l": + if i+1 >= len(args) { + continue // dangling flag without a value + } + value = strings.TrimSpace(args[i+1]) + case strings.HasPrefix(arg, "--listen="): + value = strings.TrimSpace(strings.TrimPrefix(arg, "--listen=")) + case strings.HasPrefix(arg, "-l="): + value = strings.TrimSpace(strings.TrimPrefix(arg, "-l=")) + default: + continue + } + if value == "" || strings.HasPrefix(value, "-") { + continue // malformed value — keep scanning + } + return value + } + return "" +} + func listenArgFromURL(raw string) string { u, err := url.Parse(raw) if err != nil { diff --git a/cmd/mcpproxy-tray/main_test.go b/cmd/mcpproxy-tray/main_test.go index a0fb2aa0..f10bf4ca 100644 --- a/cmd/mcpproxy-tray/main_test.go +++ b/cmd/mcpproxy-tray/main_test.go @@ -34,6 +34,58 @@ func TestBuildShellExecCommand(t *testing.T) { } } +func TestTrayListenFromArgs(t *testing.T) { + tcases := []struct { + args []string + expected string + }{ + {nil, ""}, + {[]string{"serve"}, ""}, + {[]string{"serve", "--listen", "0.0.0.0:8181"}, "0.0.0.0:8181"}, + {[]string{"serve", "--listen=0.0.0.0:8181"}, "0.0.0.0:8181"}, + {[]string{"-l", ":9090"}, ":9090"}, + {[]string{"-l=:9090"}, ":9090"}, + {[]string{"--listen"}, ""}, // dangling flag without a value + {[]string{"-l"}, ""}, // dangling short flag without a value + {[]string{"--listen", "--config", "path"}, ""}, // flag value must not be another flag + {[]string{"--listen=", "--listen", ":8181"}, ":8181"}, // empty value skipped, scanning continues + {[]string{"--listen", "--config", "--listen=:9090"}, ":9090"}, // malformed value skipped, later valid one wins + } + + for _, tc := range tcases { + if got := trayListenFromArgs(tc.args); got != tc.expected { + t.Fatalf("trayListenFromArgs(%v) = %q, expected %q", tc.args, got, tc.expected) + } + } +} + +func TestBuildCoreArgs_ForwardsCLIListenOverSocketEndpoint(t *testing.T) { + t.Setenv("MCPPROXY_TRAY_CONFIG_PATH", "") + t.Setenv("MCPPROXY_TRAY_LISTEN", "") + t.Setenv("MCPPROXY_TRAY_EXTRA_ARGS", "") + + original := trayCLIListen + defer func() { trayCLIListen = original }() + + // Regression: `mcpproxy-tray serve --listen 0.0.0.0:8181` used to drop the + // listen flag whenever the tray talked to the core over the unix socket, + // so the core fell back to the config default (127.0.0.1:8080). + trayCLIListen = "0.0.0.0:8181" + args := buildCoreArgs("unix:///tmp/mcpproxy.sock") + expected := []string{"serve", "--listen", "0.0.0.0:8181"} + if strings.Join(args, " ") != strings.Join(expected, " ") { + t.Fatalf("buildCoreArgs with CLI listen = %v, expected %v", args, expected) + } + + // Without a CLI listen flag, socket endpoints still get no --listen. + trayCLIListen = "" + args = buildCoreArgs("unix:///tmp/mcpproxy.sock") + expected = []string{"serve"} + if strings.Join(args, " ") != strings.Join(expected, " ") { + t.Fatalf("buildCoreArgs without CLI listen = %v, expected %v", args, expected) + } +} + func TestNewTrayLogConfig_DarwinUsesConsoleAndRotationDefaults(t *testing.T) { cfg := newTrayLogConfig(platformDarwin, "/tmp/tray-logs")