MCPs, APIs and CLIs: When to use what

MCP, a plain API, or a CLI: which should you reach for to give an agent a capability?

Over the past year, MCP went from a useful protocol to a reflex — the first thing people reach for the moment they want to give an agent a new capability. I think that's usually the wrong instinct, and I want to say why without claiming MCP is a mistake. It isn't. It's one of three options, and it's the one I now reach for last.

There are three ways to give an agent real capabilities right now: run an MCP server, point it at a regular API, or hand it a CLI. Here's what each one actually is, and when to pick it.

The three options

MCP is a protocol. You run a small server. The server lists the tools it offers, your agent asks what's available, and then it calls them. Every agent that speaks MCP speaks the same dialect, so you only build the integration once.

APIs are the boring kind you already know. The agent makes an HTTP call. JSON comes back.

CLIs work better than you'd expect. Models read a ton of terminal output during training, so they're shockingly fluent with the usual suspects: gh, kubectl, psql, rg, jq. Give them a shell and they figure it out.

Where MCP got oversold

MCP fixed a real headache. Before it, every team had its own way of describing tools to the model, and half the time the model called that tool with arguments it had quietly invented. MCP gives you an actual contract. Here are the tools. Here are the shapes they expect. Here's how state hangs around between turns.

Then everybody slapped an MCP wrapper on everything. A lot of those wrappers got auto-generated from an OpenAPI spec. They end up looking like this:

// Auto-generated from openapi.yaml. Loaded into context on every turn.
tools: [
  { name: "users_controller_v2_find_by_email", description: "GET /api/v2/users/by-email", inputSchema: {...} },
  { name: "users_controller_v2_find_by_id",    description: "GET /api/v2/users/:id",       inputSchema: {...} },
  { name: "users_controller_v2_update",        description: "PATCH /api/v2/users/:id",     inputSchema: {...} },
  // ... 184 more
]

That's 187 tools with auto-generated names, sitting in the agent's context whether it uses them or not, in front of an HTTP call it could have made directly. The wrapper didn't add a capability. It added a layer.

Each layer costs you something, and the costs are easy to wave off one at a time:

  • An extra hop over the protocol
  • A server you have to install, run, keep updated, and trust
  • Tool descriptions hogging context whether you call them or not
  • Yet another auth flow
  • More tokens on every turn

If all you need is one call to a REST endpoint that already works, you don't need to wrap it in an MCP.

CLIs deserve more credit

Agents are good at terminals for the same reason they're good at code. They've seen a ton of it, and the loop is fast. Run a command, read the output, run the next one. That's most of what an agent does anyway. A few things make CLIs a particularly nice fit:

  • They compose with pipes and xargs, which models have seen a million times
  • They document themselves with --help
  • They're cheap to add: no server, no protocol, just a binary on PATH
  • They're honest when they fail. Exit codes and stderr are easy to read and parse.

The catch is real: you're handing shell access to something that occasionally makes things up. That's a reason to sandbox it and scope its permissions, not a reason to avoid CLIs — just don't point one at production on the strength of a good demo.

When to use what

Use a CLI when a good one already exists and the agent is working somewhere it can run. A repo, a cluster, a database. Git, GitHub, Kubernetes, psql, that kind of thing. It's faster, and you don't have to write any glue.

Use a direct HTTP API when the agent needs to hit one specific endpoint. There's no reason to wrap a single REST call in an MCP server. Just give the agent a typed function that makes the HTTP call.

Use MCP when the integration really does need to reach a lot of places. Different clients, different agents, different users. Real surface area, real state. Hosted services that need to plug into lots of agents. An internal tool platform shared across teams. The cases where standardizing the protocol actually pays for itself.

import { Server } from "@modelcontextprotocol/sdk/server/index.js";

const server = new Server({ name: "support-platform", version: "1.0.0" });

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [createTicket, addNote, escalate, searchCustomer /* ... */],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) =>
  dispatch(req.params.name, req.params.arguments),
);

You're running a server now. That's worth the trouble when the same set of tools has to serve clients you don't own.

So my rule of thumb is to reach for MCP last, not first. If you just need to give the agent one capability, an API or a CLI call is almost always the right answer. MCP earns its keep at one point: when you'd otherwise be writing the same integration five times for five different clients.

Did we go too far with MCPs?

I think we overshot, which isn't the same as saying it was a mistake. New abstractions almost always overshoot before they find their level — people adopt them everywhere first and learn the boundaries second. That's just how a useful idea gets absorbed.

My guess at where it settles: MCP for the integrations that genuinely need a protocol, plain APIs for the calls you make from your own code, and CLIs quietly picking up more of the work than anyone expected. The agents just happen to be good at them. I could be wrong about the proportions. I'm fairly confident about the direction.

Tilo Mitra

Tilo Mitra

@tilomitra

I'm a software engineer and engineering manager living in Toronto, Canada. I currently work at Square. Read more »