Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions cmd/mcpproxy-tray/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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 <addr>`, 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()
Expand All @@ -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()
Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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 <addr>`
// (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 {
Expand Down
52 changes: 52 additions & 0 deletions cmd/mcpproxy-tray/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading