Rafe Blandford

One API, one MCP, one CLI

The one-ness at the heart of RafeOS is a shared API, MCP and CLI that make it surface agnostic.

Rafe Blandford
6 min read
RafeOS — One API, one MCP, one CLI

The single biggest design decision in RafeOS I'd highlight: build the shared thing once. Today that's over a hundred tools behind one personal API and MCP, reachable from Claude, Claude Code, the command line, the Cockpit, Telegram and ChatGPT. Here's what that looks like in practice.

Every personal-automation project hits the same fork early on. You wire up your tasks. Then you want your calendar, so you wire that up too, with its own little script, its own auth, its own way of being called. Then health, then messages, then the weather. Six months in you don't have a system; you have a drawer of one-offs, each with its own token, its own URL, its own command, its own config file.

I didn't want to build RafeOS that way. There's deliberately one API, one MCP umbrella, and one CLI dispatcher. The pay-off is the thing I find most satisfying about the system now: well over a hundred tools in reach from wherever I happen to be working. Same tools, same data, many surfaces. Everything below follows from taking that one rule seriously.

The shape of it

RafeOS is a single modular service. The modules are the obvious life areas (tasks, messaging, reading, health, home, info, calendar, email, notify, screen-time) plus a briefing module that orchestrates over the rest. They all live in one application and deploy as one unit. The headline isn't the module count; it's that those modules together expose more than a hundred tools, and every one is callable from any of the six surfaces without me re-plumbing anything.

The conventions are deliberately boring, because boring is what makes them predictable for an agent. Endpoints are /v1/<module>/<verb>. The matching MCP tools are <module>_<verb>. The CLI is rafeos <module> <verb>. The verbs are a small, reused vocabulary (search, history, today, recent, list, summary, add, draft, send), so once you know how one module behaves you can mostly guess the next. rafeos tasks add "call HMRC" does what it says. So does rafeos messaging search. The agent doesn't have to learn a new grammar per module, and neither do I.

The rafeos command line

The same boring verb pattern across modules, with rafeos --help listing them.

Every mutation carries an audit attribution header, so the store records not just what changed but who changed it: human:cli, agent:briefing, and so on. That single header is what makes the automation trustworthy rather than spooky.

A new module in about a day

The clearest test of whether the shared-thing-once bet is paying off is how cheap the next module is. The most recent one I added is digital wellbeing, screen-time data rolled up into a single score I can glance at. Data collection was in place in under a day. The verbs, the audit header, the CLI dispatch and the MCP exposure all came for free, because they're conventions the whole system already follows. What was left was the bit actually specific to screen time.

Why one service and not many small ones

The reflex for anyone who's built distributed systems is to reach for microservices. Nine-or-so modules, nine-or-so services, each independently deployable; it sounds like the grown-up choice.

I deliberately didn't. For a single-user system that one person operates, microservices buy you isolation I don't need and hand me operational overhead I'd rather not carry: lots of things to deploy, lots to monitor, lots of ways for the seams to fail. A modular monolith gets me the boundary I actually want (clean module edges, shared conventions) without the tax. I'll split something out the day there's a concrete reason to, like a module that needs to scale independently or run somewhere else.

Conventions, not a framework

Here's the call that surprised people I've talked to about it. With this much repetition across modules, the obvious move is to factor it out: write a rafeos-platform-sdk that every module imports, so the patterns are enforced rather than followed by hand. I chose not to.

The conventions live in my head and in a project CLAUDE.md the agent re-reads each session, not in an abstraction. Each module follows the pattern by hand. That sounds like the naive choice, and for a team of thirty it probably would be (shared code stops thirty people drifting). But for a system built collaboratively with an agent, hand-written, slightly-repetitive code is easier to evolve than a clever shared layer.

CLI versus MCP: horses for courses

This is the one I'd most want a semi-technical reader to take away, because it cuts against the grain of how MCP sometimes gets talked about.

MCP is the fashionable answer to "how does an agent call your tools", and the easy instinct is to expose everything over it, load the connector everywhere, and call it consistent. The reason not to is cost. Loading the full RafeOS MCP surface into a Claude Code session spends a meaningful chunk of tokens per session, just to put the tool definitions in front of the model before it's done a single useful thing. In a long coding session on the system itself, that's a standing tax on every turn, for tools I might not even reach for.

So the principle I landed on is a dual setup:

  • Cowork is for more knowledge-based work, with back-and-forth thinking. MCP is the only way to get the tools in there. The RafeOS MCP is also used by Claude Chat, ChatGPT and Grok.
  • Claude Code does a lot of technical execution, so there the agent calls the same API through the rafeos CLI via Bash (which it can already shell out to) at essentially zero token overhead, with a tiny skill telling it the command vocabulary exists. The CLI is also used by Codex and OpenClaw.

So it's a case of MCP where it's appropriate, not as a universal interface. Because it's the same API underneath either way, I can make the choice per-surface without duplicating any logic, which is the whole point of building the shared thing once.

Composed views over a thin base

At a high level there are two kinds of tool that sit on each module:

  • Primitives mirror the upstream API roughly one-to-one: thin wrappers for ad-hoc exploration, for when I want the raw thing.
  • Composed views answer an actual use-case question by rolling several calls together. gmail_action_needed instead of "list threads, then filter, then check read state". health_summary instead of fetching readiness and sleep and activity separately and stitching them in my head.

Both are available on the CLI and over HTTP, but MCP exposes only the composed ones, deliberately, to keep the agent's tool-picking surface small and focused on questions worth asking rather than handing it fifty primitives to get lost in.

That pattern grew out of a pain point; the health module grew to around 25 tools, and the agent started picking badly, reaching for an expensive live call when a cheap cached read would do, or the wrong primitive entirely. The fix had two parts. First, source-tag docstrings: every tool's description opens with a tag ([STORE] for a sub-millisecond local read, [LIVE] for something that hits an upstream service, [ON-DEMAND], [PRIMITIVE]) so the agent can judge cost and latency without reading the implementation. Second, once a module passes roughly ten tools it gets a <module>_overview tool, a map the agent can consult before committing to a call.

What earns its way to self-hosted

Building everything yourself is its own failure mode, and I've tried to resist it. The default is: don't replace something that already works, especially when the cost of using them is reasonable. The Anthropic-hosted connectors and third-party MCPs I use for a couple of sources stay exactly where they are, because there's no concrete reason to reimplement them and "I'd rather own it" isn't enough on its own.

As I've written elsewhere in this series, saying no is an important discipline in itself. I will admit that building for yourself also tends to reward the opposite instinct: prototype the thing, see if it's useful, tidy it up later.

Calendar and Gmail did earn promotion to self-hosted because I wanted consistency across surfaces and greater consideration of token efficiency, given how often they were being used. Additional customisation was also a benefit, and coherence with RafeOS conventions probably pushed it over the edge.

What the one rule buys you

Strip out the specifics and there's one move underneath: build the shared, expensive thing once, keep the module edges clean, and make it agnostic about which surface — or which actor, me or an agent — is doing the calling.

It's composable architecture pointed at my own data and services, and it's the whole reason the thing feels like a system rather than a pile of scripts. Every surface is cheap because the engine was expensive exactly once; the next module costs a day because the last hundred tools already paid for the conventions. The alternative was the drawer of one-offs I started this post worried about... and that one rule is the only thing standing between the two.


On how this was made: RafeOS is built collaboratively with AI, and this post was drafted with Claude and edited by me. All part of a wider experiment.

New writing by email

Occasional pieces on product, technology and AI — and how they actually play out in practice.

Thanks — check your inbox to confirm.