Your CLI Tool Needs an MCP Server: Shipping One in a Day, Protocol by Hand A developer shipped `xbrowser mcp`, a stdio Model Context Protocol server that exposes seven browser-automation tools from the xbrowser CLI to MCP-capable clients like Claude Desktop and Cursor. The implementation hand-writes the JSON-RPC protocol layer instead of using the official SDK, reusing the CLI's existing executeChain/executeCommand functions so the MCP layer stays a pure translation concern. The build log documents two integration bugs — the CLI consuming stdin that doubles as the protocol channel, and a shutdown race that killed in-flight requests — plus a 50-line smoke script for testing MCP servers without a full client. Published on 2026-09-11 · xbrowser v1.23.1 Every CLI tool built before 2026 is asking itself the same question: how do AI agents discover me? The answer the ecosystem converged on is the Model Context Protocol — MCP. If Claude Desktop, Cursor, or any MCP-capable client can list your tool's capabilities and call them, your tool exists for agents. If not, it doesn't. xbrowser https://github.com/dyyz1993/xbrowser is a browser-automation CLI 57 commands, 130+ site plugins, self-healing replay . This week we shipped xbrowser mcp — a stdio MCP server exposing 7 browser tools. This post is the build log: the design decisions, the two real bugs the first smoke run caught, and why we wrote the protocol layer by hand. The official @modelcontextprotocol/sdk is fine software. We didn't use it, for three reasons that generalize: initialize , tools/list , tools/call , plus an error path. That's a switch statement — about 60 lines including the newline-delimited framing. The SDK's value is in the long tail resources, prompts, transports, typed helpers that a 7-tool server doesn't touch. The whole protocol layer is small enough to hold in your head — which is exactly the property you want in the layer agents depend on. The key decision: the MCP server rewrites no automation logic. Every tool call bottoms out in the same executeChain / executeCommand functions the CLI uses: js case 'browser navigate': { const url = args.url as string; let chain = goto ${url} ; if args.chain chain += && ${args.chain} ; // follow-up commands as a chain const result = await executeChain chain, { sessionName: session } ; return text { ok: result.success, steps: ... } ; } This buys three things: one execution path to test the CLI's 4000-test suite already covers it , one place to fix bugs, and automatic feature parity — when the replay engine gains a new healing strategy, MCP users get it the same day. The MCP layer is a translation concern JSON-RPC ↔ CLI , and translation layers should be boring. The 7 tools were chosen for agent ergonomics, not CLI symmetry: browser navigate with optional follow-up chain , browser act click/fill/press/… , browser read text/html/title , browser snapshot accessibility tree — the token-efficient page view , browser screenshot , browser network request inspection , and browser replay the self-healing replay engine — our differentiator, exposed as a single tool call . Both are the kind of thing you only find by running the real thing. Bug 1: the CLI ate the protocol. Our entrypoint reads stdin when piped — that's how echo "goto x && title" | xbrowser works. To an MCP client, the server's stdin is the protocol channel. The first test sent initialize and the CLI helpfully tried to execute {"jsonrpc":"2.0"...} as a browser command. Fix: mcp short-circuits stdin collection; protocol lines and command lines never mix. Bug 2: the shutdown race. Our smoke test piped requests from a file; EOF arrived while the browser was still launching for tools/call . The stdin end → process.exit 0 handler killed the in-flight request mid-flight. Real clients keep stdin open, so they'd never hit it — but a server that dies with pending work is wrong on principle. Fix: an inflight counter; exit only when stdin is closed and nothing is pending. Requests finish, then the process leaves. Neither bug appears in the SDK's examples, because both live at the integration boundary between an existing CLI and the protocol — which is exactly where your bugs will live too. You don't need Claude Desktop to test an MCP server. A 50-line smoke script spawns the server, writes four JSON-RPC lines initialize, tools/list, unknown method, unknown tool , and asserts the responses: server info correct, exactly 7 tools, -32601 for bad methods, isError content for bad tools. That's now in the repo as scripts/mcp-smoke.mjs and takes 2 seconds to run. Protocol conformance is testable with plain processes — no client required. For the real thing: claude mcp add xbrowser -- xbrowser mcp , then ask any MCP-capable agent to open a page and snapshot it. If you maintain a CLI with an agent-relevant surface browser, files, HTTP, search — most CLIs qualify , an MCP server is probably a day of work: enumerate 5–10 verbs agents actually need, wire them to your existing functions, hand-write the protocol loop, smoke-test with processes. The hard part isn't the protocol — it's the boundary decisions: which verbs, what granularity, how errors surface as tool results instead of crashes. xbrowser is MIT-licensed: https://github.com/dyyz1993/xbrowser https://github.com/dyyz1993/xbrowser · npm: https://www.npmjs.com/package/@xbrowser/cli https://www.npmjs.com/package/@xbrowser/cli This post was drafted through xbrowser's own content pipeline, and the browser tools above are the same ones any MCP agent gets.