Skip to content

fix(tray): forward --listen to the spawned core process - #1015

Merged
github-actions[bot] merged 2 commits into
smart-mcp-proxy:mainfrom
HaloCollar:fix/tray-forward-listen
Aug 24, 2026
Merged

fix(tray): forward --listen to the spawned core process#1015
github-actions[bot] merged 2 commits into
smart-mcp-proxy:mainfrom
HaloCollar:fix/tray-forward-listen

Conversation

@electrolobzik

Copy link
Copy Markdown
Contributor

Bug

mcpproxy-tray serve --listen 0.0.0.0:8181 silently drops the --listen flag on macOS (reproduced on tray v0.43.0):

  • The tray log shows the core spawned as /bin/zsh -l -c exec '/mcpproxy' 'serve' — no --listen.
  • lsof confirms the core then listens on the config-file value (default 127.0.0.1:8080); nothing ever opens the advertised port, so any tooling that probes it concludes the daemon is down.

Root cause

cmd/mcpproxy-tray/main.go never parses its own command line — serve --listen … is ignored entirely. On macOS resolveCoreURL() prefers the unix socket for tray↔core communication (#102), and buildCoreArgs() only derives --listen for TCP/HTTP core URLs, so on the socket path the core is spawned with no --listen at all and falls back to the config default.

Fix

  • New trayListenFromArgs() parses --listen/-l (both space and = forms) from the tray's argv; other args remain ignored as before. Malformed values (dangling flag, empty = form, value starting with -) are skipped and scanning continues to the first valid value.
  • buildCoreArgs() now always forwards an explicit CLI --listen to the spawned core's argv — including when the tray talks to the core over the socket/pipe, since the socket only covers tray↔core communication while the core must still open the advertised TCP address. Both the login-shell-wrapped spawn and the direct-exec fallback consume the same argv.
  • URL/env-derived --listen behavior for TCP endpoints is unchanged.

Tests

  • TestTrayListenFromArgs covers both flag forms plus the malformed-input regressions (--listen --config path, --listen= --listen :8181, dangling -l, continue-scan to a later valid --listen=:9090).
  • TestBuildCoreArgs_ForwardsCLIListenOverSocketEndpoint — regression: CLI listen forwarded over a socket endpoint; no --listen on socket endpoints without the flag.
  • CI on our fork built all platform artifacts (incl. darwin-arm64) green: fix(tray): forward --listen to the spawned core process HaloCollar/mcpproxy-go#7

🤖 Generated with Claude Code

Roman Chernyak and others added 2 commits August 20, 2026 20:20
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 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved (Model B): Paperclip review verdicts = ACCEPT and qa-gate green at this head SHA. Arming auto-merge; GitHub merges when all required checks pass.

@github-actions
github-actions Bot merged commit 44e3cf9 into smart-mcp-proxy:main Aug 24, 2026
36 checks passed
@Dumbris

Dumbris commented Aug 24, 2026

Copy link
Copy Markdown
Member

Merged — thanks Roman! Great catch that the tray never parsed its own argv, so mcpproxy-tray serve --listen 0.0.0.0:8181 was silently dropped on the macOS socket path and the core quietly fell back to 127.0.0.1:8080; the fix is tightly scoped and the malformed-flag test cases are a nice touch. 🙏

Also useful method is to set using env var https://docs.mcpproxy.app/configuration/environment-variables#setting-tray-variables-on-macos

github-actions Bot pushed a commit that referenced this pull request Aug 25, 2026
…core-URL semantics for pinned --listen (#1036)

* fix(tray): preserve host in normalizeListen + sane port-conflict and core-URL semantics for pinned --listen

Three follow-ups to PR #1015 (tray --listen forwarding):

1. normalizeListen("127.0.0.1:8181") returned ":8181", silently rebinding a
   loopback-pinned address to every interface — and that value is forwarded
   verbatim as the core's --listen from both the MCPPROXY_TRAY_LISTEN path and
   the new CLI path. It now never widens a bind: a pinned host is preserved
   (IPv6 included), a bare port defaults to loopback (matching
   listenArgFromURL's rule for derived addresses), an explicit all-interfaces
   request is honored, and unparseable input passes through for the core to
   reject.

2. handlePortConflictError bumped the port and relaunched, expecting
   buildCoreArgs to derive the new one from coreURL — but an explicit CLI
   --listen always wins, so the retry would re-hit the same busy port forever.
   An explicitly pinned listen is now treated as a user contract: fail loudly
   (error log + state-machine error + tray notification) instead of silently
   retrying or silently moving off the pinned address. Unpinned setups keep the
   auto-bump. The choice is documented at the call site.

3. resolveCoreURL ignored the CLI listen on the TCP-fallback path, so the core
   bound the pinned port while the tray probed the default. It now takes the
   parsed CLI listen (highest priority, above MCPPROXY_TRAY_LISTEN and
   MCPPROXY_TRAY_PORT); wildcard binds are dialed on loopback.

* fix(tray): percent-escape IPv6 zones in the derived core URL

Cross-model review finding on the new coreURLFromListen(): it built the
URL by string concatenation, so a zoned IPv6 listen such as
"[fe80::1%en0]:8181" produced "http://[fe80::1%en0]:8181" — which
url.Parse rejects outright with `invalid URL escape "%en"`. Every later
parse of the core URL (readiness checks, listenArgFromURL, the
port-conflict handler) would then fail on an address the function was
specifically meant to support, since it already claimed IPv6 handling.

Build the URL through url.URL instead, which escapes the zone
("fe80::1%en0" -> "fe80::1%25en0") and round-trips through url.Parse.

Adds the zoned case to the coreURLFromListen table plus a
TestCoreURLFromListen_AlwaysParseable invariant asserting that every
derived core URL survives url.Parse with the expected port.

Also tightens docs/tray-debug.md: an all-interfaces value supplied via
MCPPROXY_TRAY_LISTEN / MCPPROXY_TRAY_PORT is narrowed to loopback
(the core's --listen is derived from the URL the tray dials), so the
tray's own --listen flag is the way to ask for a LAN-exposed bind. The
previous wording implied the variables forwarded any host as written.

* fix(tray): surface an unparseable explicit listen instead of dropping it

Second cross-model review round. Before the resolveCoreURL rework, an
unparseable MCPPROXY_TRAY_LISTEN / --listen produced an unparseable core
URL, so listenArgFromURL returned "" and buildCoreArgs fell through to
the raw env value — handing the core a bad --listen that it rejected
loudly. Now coreURLFromListen returns "" for such a value and
resolveCoreTCPURL falls through to the valid default endpoint, so
listenArgFromURL succeeds and the explicitly configured address is
dropped without a trace. Worse, when a core is already listening on that
default the tray attaches to it and looks like it honoured an address it
actually ignored.

Add unparseableListenSources(), which reports every explicitly set
listen value that cannot be turned into a dialable endpoint, and log each
one at Warn from main() next to the resolved core URL. This keeps
normalizeListen's stated contract — unparseable input is surfaced rather
than silently rewritten — on the path that no longer reaches the core.

Behaviour is otherwise unchanged: valid values (bare port, pinned host,
wildcard, IPv6) report nothing, and blank/whitespace values count as unset.

* fix(tray): make the unparseable-listen warning state what actually happened

Third cross-model review round. The warning added in the previous commit
claimed the value was "ignored" and that the tray fell back to "the
default core endpoint". Both can be wrong: with an invalid --listen and a
valid MCPPROXY_TRAY_LISTEN, resolution falls through to the env endpoint
rather than the default, and the invalid CLI value is still forwarded to
the launched core by buildCoreArgs, so it is not ignored either.

Reword to the accurate, neutral claim — the value cannot form an endpoint
the tray can dial and therefore did not determine the core URL — and keep
the resolved core_url field so the log still shows which endpoint won.
No behaviour change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants