Essay

MCPs, APIs and CLIs: When to use what

Three ways to give an agent capabilities, and how to pick between them.

There are three ways to give an agent real capabilities right now. You can run an MCP server, point it at a regular API, or hand it a CLI. Let's explore when you should use each.

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 a wall of tools, all with weird auto-generated names. They sit in the agent's context whether it uses them or not, in front of an HTTP call the agent could have made directly.

Every wrapper costs you something:

  • 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
  • Expensive in terms of tokens

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 that you're handing shell access to something that sometimes makes things up.

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.

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 when you'd otherwise be writing the same integration five times.

Did we go too far with MCPs?

I think so. New abstractions always overshoot before they find their level. The mix we'll probably settle on looks like this: MCP for the integrations that genuinely need a protocol, plain APIs for the calls you make from your own code, and CLIs picking up more of the work than anyone expected. The agents just happen to be good at them.

Tilo Mitra

Tilo Mitra

@tilomitra

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