porthub.mcp: the menu bar app that registered itself as a route
A short one, about 188 lines that made the whole portless setup feel complete.
The problem they solve: AI coding agents on this machine constantly trip over the proxy. An agent starts a dev server, the route doesn't appear, or a stale route shadows a fresh one, and the agent — which cannot see a menu bar — starts theorising. Everything porthub shows me in a popup was invisible to the tools doing half the work here.
So porthub now embeds an MCP server. Five tools, exactly mirroring the
UI: get_status, list_routes, start_proxy, stop_proxy,
kill_route. The handlers are the same functions the tray menu and
popup call — the MCP surface and the human surface cannot drift apart,
because there is only one surface with two faces. kill_route refuses
static aliases (pid: 0) just like the UI does, and the start/stop
tools warn honestly in their descriptions that a macOS sudo dialog will
appear — an agent can invoke it, but a human has to bless it. That
division of authority fell out naturally and I'd now choose it on
purpose: agents propose, the password prompt disposes.
The part I'm disproportionately pleased with is the addressing. The MCP
server binds 127.0.0.1 on a random port — and then registers
itself into the proxy's routes.json as a static alias:
filtered.push({ hostname: 'porthub.mcp.localhost', port: mcpPort, pid: 0 });
So the app that exists to manage portless routes is itself reachable as
a portless route: any MCP client on the machine connects to
porthub.mcp.localhost — stable name, no port number — which resolves
through the very proxy porthub monitors. The tool eats its own dog food
at the addressing layer. On quit, it politely removes its own route; a
watcher re-asserts the registration if routes.json gets rewritten
underneath it, which — given who rewrites that file — is porthub
guarding itself against porthub.
Transport is MCP's streamable HTTP with per-session server instances keyed by session ID, torn down on close. Nothing clever; the SDK does the work.
There's a self-referential catch worth noting for anyone copying this:
reaching porthub.mcp.localhost requires the proxy to be up, and two
of the five tools exist precisely for when it isn't. An agent that needs
start_proxy may not be able to reach the tool that provides it. In
practice the failure mode is rare — porthub keeps the proxy alive as its
whole job — but a belt-and-braces client could keep the raw
127.0.0.1:port as a fallback address. I've decided to live dangerously
and enjoy the clean hostname.
The bigger observation: giving a piece of dev infrastructure an MCP endpoint took under two hundred lines and one afternoon, and the payoff is that route confusion stopped being a category of agent failure — the agent just checks. The same reasoning that says every service deserves a health endpoint now argues that every local dev tool deserves an MCP endpoint. The marginal cost has gotten absurdly low; the marginal benefit — an entire class of "the AI is guessing about system state" eliminated — has not.