Porthub #mcp #dev-tools

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.