Porthub #ai #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.