skip to content
Terry Li

I had a working MCP server for browser automation. Cookie injection, stealth patches, navigator fingerprint spoofing — the whole thing running over stdio. It worked. I shipped it weeks ago.

Today I deleted the MCP surface and replaced it with a CLI.

The trigger was practical. I wanted to run the tool from one machine against a browser on another, over SSH. MCP-over-SSH is stdio transport, which means the server dies when the connection dies. Browser state dies with it. No hot reload — if the process crashes, the whole Claude Code session needs restarting. The underlying operations don’t actually need any of that — they’re stateless calls into a separate browser daemon that holds the real state.

But the deeper reason was a rule I’d been ignoring: pick the reversible direction.

The rule

When you genuinely can’t decide between two designs, pick the one you can undo.

A CLI wraps into an MCP tool with about three lines of code:

@mcp.tool()
def browser_navigate(url: str) -> dict:
result = subprocess.run(["trogocytosis", "navigate", url], capture_output=True)
return json.loads(result.stdout)

An MCP server does not unwrap into a CLI. You have to rewrite it — extract the handler functions, figure out which modules depend on the FastMCP context, rebuild the entry point, redesign the argument parsing. It’s a day of work if you’re lucky.

The consequence: if I pick CLI and I’m wrong, I pay for three lines of code to wrap it. If I pick MCP and I’m wrong, I pay for a rewrite. The asymmetry is the whole argument.

When MCP actually earns its keep

There are real reasons to pick MCP over CLI. They’re narrow:

  1. Cross-invocation mutable state that can’t live on the filesystem. A persistent browser tab, an open WebSocket, a daemon holding a compiled model in memory. If the state dies between calls and can’t be serialized, you need a long-running process, and MCP gives you one.

  2. Nested structured input. When the input is naturally a JSON object with arrays of records inside objects, CLI flags become awkward. --from alice --to bob is fine. --operations='[{"type":"transfer","amount":100,"metadata":{...}}]' is telling you to pick MCP.

Everything else — authentication, cross-client distribution, “smoother UX than subprocess” — those aren’t criteria. Authentication lives in the tool, not the transport. Cross-client works fine if the CLI is on PATH. And the UX gap between subprocess.run(["tool", ...]) and tool.method(...) is nothing compared to the rewrite cost when you pick wrong.

The tiebreaker

If you honestly can’t decide, CLI wins. Not because CLI is better — because you can change your mind.

The temptation when you’re building an agent-facing tool is to reach for MCP first. It’s the current-year answer. It’s the thing with the specification and the docs and the tutorials. MCP is the fashionable layer, and fashion pulls hard.

But fashion is not a design criterion. Reversibility is.

What trogocytosis looks like now

The refactor was smaller than I expected. The browser/cookies/stealth modules were already pure functions with flat inputs. They didn’t know they were being called from an MCP tool. I added a thin cyclopts CLI layer over the same modules, pointed the entry script at it, and kept the MCP server as a subcommand (trogocytosis serve) for the one case where I still need it.

The diff was about 150 lines. Most of it was CLI command definitions. The core logic didn’t change at all.

Now I can run it from anywhere:

Terminal window
ssh mac "trogocytosis login linkedin.com"
ssh mac "trogocytosis navigate 'https://www.linkedin.com/in/someone/details/experience/'"
ssh mac "trogocytosis snapshot"

Each call is independent. If the SSH drops, nothing dies. If the tool crashes, I restart one command, not a whole session. The browser daemon persists across calls because that’s its job, not trogocytosis’s job.

And if I decide tomorrow that I actually need the MCP server back for some reason — it’s already there. trogocytosis serve still works. I never gave anything up.

That’s the whole point. The reversible direction doesn’t commit you. The other one does.