Repository navigation
Local MCP: moving the port after a bind conflict keeps the capability path #23
Description
Recovering from a local-MCP bind conflict keeps the profile's capability path.
That is deliberate today — reassignLocalMcpPort says so
(src/main/profileStore.ts:376-387): only the port moves, so a configuration
the user already pasted stays valid for the profile it named, and they re-copy
one line instead of re-pasting a secret. This issue is about whether that trade
still holds in the one case where the conflict was not benign.
The path a hostile local process could take
- A user has a client configured with
kozou-local-<profile>at
127.0.0.1:<port>/mcp-<hex>. The path sits in that client's config file in
plaintext, whichEGRESS.mditem 10 already states. - The app is not running. Another local process binds
<port>and answers on
any path. The configured client connects on its own schedule and hands the
path over. Nothing about the transport prevents this: loopback is per-machine,
the path is a capability, and there is no server authentication for a client
to verify. - The user starts the app, sees
MCP port busy, and presses move port. The
port changes; the path does not. The squatter, which now knows the path, can
scan for the new port and reach the tools.
What that reaches is schema metadata — structure, COMMENTs, @ai/@policy
annotations, function signatures. Never row data, and no write path: the MCP
worker has no execution tool and cannot construct one (EGRESS.md item 8).
So this is a metadata-confidentiality question, not an integrity one.
There is a smaller UI leg to the same story: the card stops offering to open the
config panel while the status is error-port-busy, but a panel already open
keeps rendering the snippet with only a "server currently stopped" hint
(src/renderer/src/App.svelte:482-505). A user reading that hint as "not
running, so nothing is listening" would be wrong in exactly this scenario.
What could change
- Rotate the path on recovery from a bind conflict, and tell the user the old
configuration is dead rather than letting it fail silently. Costs the current
"re-copy one line" property. - Rotate only when the conflict was observed as a foreign bind, keeping the
cheap path for an ordinary conflict (a stale instance of this app, a port the
user reassigned themselves). - Close or blank an open config panel when the status turns
error-port-busy,
independently of whichever rotation policy is chosen.
Why this is filed rather than fixed
The current behaviour is documented intent, and each option above trades user
friction against a threat that already requires a local process able to read the
user's client config files. That balance is a product decision, not a bug fix.
The README no longer implies the config is withheld while a foreign process
holds the port, and it now states that moving the port keeps the path, so the
documentation is accurate under today's behaviour either way.
Activity
Rotate the capability path when the port moves (#47)
takashi-matsuyama commentedon Aug 16, 2026
Settled by #47 (merged): recovery from a bind conflict now rotates the capability path along with the port, keeping the locator id.
Why the trade came out that way. The premise this issue reasoned from — that rotating "costs the current re-copy one line property" — does not hold once both config shapes are read:
move porthad already stopped it naming this app's server, and the panel copies whole snippets, so the same one line is re-copied either way;src/bridge/stdioBridge.ts), so it is transparent to both changes. That entry did not exist when this was filed.So the path was being kept for nobody, while a client still configured with the old URL goes on offering it to whoever holds that port now.
On the three options listed above:
EADDRINUSEcarries no identity of the holder, and probing for one either hands the real path to whatever is listening, or, with a freshly generated path, is defeated by a squatter that answers only paths it has already harvested — which is the adversary this issue describes. The "stale instance of this app" case it would protect is also narrow: a second instance is refused by the single-instance lock, leaving an orphaned worker after a force quit.mainbefore this was resolved. The panel is no longer offered while the status iserror-port-busy, and one that was already open carries a note naming the port conflict rather than a generic "not running" hint.Line references in the text above have drifted (filed 2026-08-02, before the layout rework):
reassignLocalMcpPortis nowsrc/main/profileStore.ts:523, and the UI leg moved fromApp.sveltetosrc/renderer/src/McpBlock.svelte:118-130andsrc/renderer/src/lib/statusCopy.ts:152.Closing.