From 13c3df1ec6c144dc8269fee99424521e562ac957 Mon Sep 17 00:00:00 2001 From: Roman Chernyak Date: Thu, 20 Aug 2026 20:20:32 +0200 Subject: [PATCH 1/2] fix(tray): forward --listen to the spawned core process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tray ignored its own command-line arguments entirely, so launching it as `mcpproxy-tray serve --listen 0.0.0.0:8181` silently dropped the listen address: on macOS the tray prefers the unix socket for tray<->core communication, buildCoreArgs() only derived --listen for TCP/HTTP core URLs, and the core was spawned as `mcpproxy serve` with no --listen at all. The core then fell back to the config-file default (typically 127.0.0.1:8080) and the advertised port never opened. Observed live on mcpproxy-tray v0.43.0 (tray log shows the shell-wrapped spawn without --listen; lsof confirms the core listening on 8080 instead of 8181). Fix: parse --listen/-l (space and = forms) from the tray's argv and always forward it to the spawned core's argv, even when the tray talks to the core over the socket/pipe — the socket only covers tray<->core communication, while the core must still open the advertised TCP address. URL/env-derived --listen behavior for TCP endpoints is unchanged. Co-Authored-By: Claude Fable 5 --- cmd/mcpproxy-tray/main.go | 47 ++++++++++++++++++++++++++++++--- cmd/mcpproxy-tray/main_test.go | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/cmd/mcpproxy-tray/main.go b/cmd/mcpproxy-tray/main.go index 7cf2428e..ef85750d 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,30 @@ 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. Returns "" when no listen flag is present. +func trayListenFromArgs(args []string) string { + for i := 0; i < len(args); i++ { + arg := args[i] + switch { + case arg == "--listen" || arg == "-l": + if i+1 < len(args) { + return strings.TrimSpace(args[i+1]) + } + return "" + case strings.HasPrefix(arg, "--listen="): + return strings.TrimSpace(strings.TrimPrefix(arg, "--listen=")) + case strings.HasPrefix(arg, "-l="): + return strings.TrimSpace(strings.TrimPrefix(arg, "-l=")) + } + } + 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..636efdcf 100644 --- a/cmd/mcpproxy-tray/main_test.go +++ b/cmd/mcpproxy-tray/main_test.go @@ -34,6 +34,54 @@ 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 + } + + 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") From 97aacf8bd8da61436790a0b08aa007569ee40b2c Mon Sep 17 00:00:00 2001 From: Roman Chernyak Date: Thu, 20 Aug 2026 22:59:22 +0200 Subject: [PATCH 2/2] fix(tray): skip malformed --listen values and keep scanning (review) Co-Authored-By: Claude Fable 5 --- cmd/mcpproxy-tray/main.go | 22 ++++++++++++++++------ cmd/mcpproxy-tray/main_test.go | 6 +++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/cmd/mcpproxy-tray/main.go b/cmd/mcpproxy-tray/main.go index ef85750d..0c90361f 100644 --- a/cmd/mcpproxy-tray/main.go +++ b/cmd/mcpproxy-tray/main.go @@ -929,21 +929,31 @@ func shellQuote(arg string) string { // 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. Returns "" when no listen flag is present. +// 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) { - return strings.TrimSpace(args[i+1]) + if i+1 >= len(args) { + continue // dangling flag without a value } - return "" + value = strings.TrimSpace(args[i+1]) case strings.HasPrefix(arg, "--listen="): - return strings.TrimSpace(strings.TrimPrefix(arg, "--listen=")) + value = strings.TrimSpace(strings.TrimPrefix(arg, "--listen=")) case strings.HasPrefix(arg, "-l="): - return strings.TrimSpace(strings.TrimPrefix(arg, "-l=")) + value = strings.TrimSpace(strings.TrimPrefix(arg, "-l=")) + default: + continue + } + if value == "" || strings.HasPrefix(value, "-") { + continue // malformed value — keep scanning } + return value } return "" } diff --git a/cmd/mcpproxy-tray/main_test.go b/cmd/mcpproxy-tray/main_test.go index 636efdcf..f10bf4ca 100644 --- a/cmd/mcpproxy-tray/main_test.go +++ b/cmd/mcpproxy-tray/main_test.go @@ -45,7 +45,11 @@ func TestTrayListenFromArgs(t *testing.T) { {[]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{"--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 {