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.