cd /news/ai-agents/sdk-vs-cli-vs-mcp-a-dev-story · home topics ai-agents article
[ARTICLE · art-132780] src=blog.postman.com ↗ pub= topic=ai-agents verified=true sentiment=· neutral

SDK vs CLI vs MCP, a dev story

A developer relations engineer building internal release-readiness tooling with Claude Code chose the Octokit SDK over the GitHub CLI and the GitHub MCP server for a roughly 200-line tool that makes four GitHub API calls, because the SDK's TypeScript types caught a GitHub response field rename at build time rather than at 6 AM in an unread log. The engineer found the SDK's build-time contract limits agent improvisation, since an agent cannot call methods like octokit.rest.checks.listForRef unless the calling code was already written, and noted the SDK reads its GitHub token from the process environment, which is less safe in an agent session on a laptop than in GitHub Actions with a scoped token.

by read12 min views3 publishedSep 17, 2026
SDK vs CLI vs MCP, a dev story
Image: Blog (auto-discovered)

My team builds internal software for ourselves. Not products. The unglamorous stuff that keeps a developer relations org running: a release-readiness checker, a script that reconciles our event calendar, a dashboard for pull requests that have gone quiet. Most of it gets written by Claude Code, and lately a fair amount of it gets run by Claude Code too.

Last month I hit a decision I’d been putting off. The release-readiness tool needs data out of the GitHub API. I know how to call that API. What I didn’t know was how to hand that ability to an agent, and there were three doors in front of me, all opening onto the same endpoints.

Door one: install the Octokit SDK and have the agent write TypeScript against it. Door two: install gh and let it run shell commands. Door three: connect the GitHub MCP server and let it call tools.

I picked door one, then door three, then changed my mind again. What finally settled it wasn’t a benchmark. It was noticing I’d been asking the wrong question.

What I’m actually building #

Every morning the tool answers one question: is this repository safe to cut a release from?

Underneath that it’s four API calls. Open pull requests, filtered down to the ones that haven’t moved in a week and aren’t drafts. Check runs on the latest commit on main. Any draft release that already exists. Then a short summary written to a file I read over coffee.

About 200 lines. The code isn’t interesting. What turned out to matter is who runs it, and that varies more than I expected. Some mornings I run it by hand. Most mornings a cron job does. And every couple of weeks I’m sitting in a Claude Code session asking something fuzzier, like why last Thursday’s release got held up, where the agent needs to poke around GitHub in ways I never anticipated when I wrote the script.

Those three callers want different things, which is the part I missed.

Door one: the SDK #

An SDK is a typed client library for one language. Install it, get autocomplete, and the compiler catches your typos.

import { Octokit } from "@octokit/rest";

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

const { data: pulls } = await octokit.rest.pulls.list({
  owner: "postman-devrel",
  repo: "release-tools",
  state: "open",
  per_page: 100,
});

const stale = pulls.filter((pr) => {
  const daysSinceUpdate = (Date.now() - Date.parse(pr.updated_at)) / 86_400_000;
  return daysSinceUpdate > 7 && !pr.draft;
});

console.log(`${stale.length} stale pull requests`);

This is what I wrote first and it’s still what the cron job runs. The types earn their keep. GitHub renamed a field in a response payload last spring and TypeScript caught it at build time rather than at 6 AM in a log nobody was reading.

Where it falls down is improvisation. An SDK is a build-time contract, so for the agent to use it, somebody has to have written the calling code already. Sitting in a session asking about Thursday’s release, the agent can’t reach for octokit.rest.checks.listForRef unless I exposed it. It can write fresh code, and it will happily do that, but then I’m compiling throwaway TypeScript to answer a question I’ll only ask once. I did that twice before admitting it wasn’t working.

Credentials are the other thing. The SDK reads a token from the process environment, so the secret lives wherever the process lives. Fine for GitHub Actions with a scoped token. Less fine when the process is an agent session on my laptop.

Door two: the command-line interface #

A command-line interface (CLI) wraps the same API in commands you run from a terminal. GitHub’s is gh, and it does one thing the SDK can’t: it works before anyone writes integration code.

gh pr list \
  --repo postman-devrel/release-tools \
  --state open \
  --json number,title,updatedAt,isDraft \
  --jq '[.[] | select(.isDraft == false)] | length'
6

Claude Code was already fluent in it, which surprised me more than it probably should have. Nobody had to teach it gh. When it wasn’t sure about a flag it ran gh pr list --help and read the output the way a person would. The terminal is the interface agents already have, so there’s no adapter to write and nothing to configure.

It’s also close to free. A CLI costs nothing in your context window until the moment the agent runs it, and the response is whatever you asked for. That --jq filter means one integer comes back instead of six pull request objects. Try getting a tool that owns its own response shape to do that.

The costs are real too. Output is text, so something has to parse it. Errors show up as exit codes and stderr instead of typed exceptions. And gh authenticates as exactly one identity, which is what you want on your own machine and the wrong answer for anything twelve people share.

Door three: the MCP server #

The Model Context Protocol (MCP) is a standard for letting a model discover and call tools. A server advertises what it offers, and the client loads those definitions into the model’s context so it knows they exist.

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp/"
    }
  }
}

Two lines of configuration and the agent could read pull requests, check runs, releases, issues, and plenty besides. For the improvisational case it was the best experience by a distance. Nothing to shell out to, nothing to parse, structured arguments in and structured results back.

Then I looked at the bill before I’d typed a word.

The client loads every tool definition up front. Somebody measured the GitHub MCP server at roughly 42,000 tokens of schemas, argument types, and example payloads. A survey of more than 3,000 servers put the median nearer 1,900, but medians aren’t what you install. The servers people actually reach for are the fat ones, and three of them together can eat over 10% of a 200,000 token window before the conversation starts. That’s context I’d rather spend on the repository.

So stop everything:

{
  "mcpServers": {
    "github": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
        "ghcr.io/github/github-mcp-server",
        "--toolsets", "pull_requests,repos",
        "--read-only"
      ],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
      }
    }
  }
}

Scoping to two toolsets and adding --read-only cut it down a lot. That flag skips every write tool, which for a morning report that mutates nothing is cheaper and safer than any instruction I could put in a prompt. Relief is coming at the protocol level as well, since clients have started deferring tool definitions and letting the model search for them on demand once they cross some share of the window.

What kept MCP in the mix wasn’t the ergonomics, though. It’s the only one of the three with an authorization model that survives more than one user. The protocol carries per-user auth, so the server acts as whoever is asking and can log it. I can’t rebuild that with a shell command, and the day our tooling outgrows my laptop is the day I need it.

How I actually decided #

I stopped asking which interface is best. The better question is who calls it, and that produced a table I now keep in the repository’s README:

Question SDK CLI MCP server
Who calls it Your application code An operator or an agent in a shell An agent through a client
Known at build time Yes No No
Context cost when idle None None Tool definitions, always loaded
Response shape Typed objects Text you filter Structured results
Credentials Process environment Local machine auth Per-user, with audit logging
Best fit Shipped integrations Unattended agent work Interactive and multi-user work

For the release checker the answer stopped being one door. The cron job runs SDK code, because I want a compiler and I want it to fail loudly at build time. Interactive sessions use gh, because it costs nothing until it’s used and Claude Code needed no help from me. The MCP server goes on for exploratory work across repositories where I can’t predict the calls.

That felt like a cop-out when I first wrote it down. It isn’t. Three consumers, three surfaces.

Gotchas I’ve hit #

Count your tool definitions before you blame the model

I spent an afternoon convinced Claude Code had gotten worse at a task. It hadn’t. I’d added two MCP servers that week, and between them they’d quietly displaced the part of the context that used to hold the repository’s file tree. Check what your servers cost before you start rewriting prompts. The MCP specification’s tools section is worth an hour of your time so you know what’s being sent on your behalf.

A CLI that prints JSON is a different tool from one that prints tables

gh pr list renders a table for humans. gh pr list --json returns data. Agents cope with the first and are reliable with the second. If you generate a CLI for agents to drive, make structured output a first-class flag and document it in --help, because --help is where the agent will look.

Read-only mode is the cheapest safety win available

Almost everything my tooling does is read. Turning writes off removed a whole category of mistake I’d otherwise be prompting my way around, and it shrank the tool definitions at the same time.

Don’t hand-maintain three surfaces

This is the one that changed my mind about the problem. I nearly wrote a small wrapper CLI around our internal API so agents could drive it. Then I thought about keeping that wrapper in sync with the SDK, and both of them in sync with the API, by hand, indefinitely, and I closed the editor.

One specification, three surfaces #

So the three aren’t rivals. They’re projections of the same API aimed at different consumers, and the only sane way to have all of them is to generate them from one specification instead of writing and maintaining each by hand.

That’s the part I’d been dreading, and it turns out to be mostly solved. Not by one tool, though, which took me a while to accept.

Fern for the SDK and the CLI

Fern reads an OpenAPI file and produces both surfaces a human touches. Type-safe SDKs across nine languages, and a CLI published to npm with progressive disclosure and automatic versioning.

npm install -g fern-api
fern init --openapi ./openapi.yml
fern generate

Both targets sit in the same generators.yml group, so one fern generate rebuilds them together and they can’t drift apart. That was the exact objection that nearly had me hand-writing a wrapper, and it goes away.

What Fern won’t do is generate an MCP server for your API. It hosts a documentation MCP server, which sounds like the same thing and isn’t. That one answers questions about your API. It doesn’t call it. Genuinely useful when an agent is trying to work out how your endpoints fit together, but it isn’t the third surface.

The Postman MCP Generator for the MCP server

For that, Postman’s MCP Generator does the work. You pick the requests you want exposed, Postman turns each one into a tool, and you download a server that works with MCP-compatible agents including Claude Code, Cursor, and VS Code Copilot.

One constraint to know before you plan around it: the generator is documented against requests from the Postman API Network, so the smooth path today is for APIs already published there. For something purely internal you’re importing the specification into a workspace first, and I’d check that route works for you before building a pipeline on it.

Worth knowing that Postman also runs an MCP server for its own platform and ships a Claude Code plugin, so the agent side of this is well travelled.

Wiring the two together

The join between them isn’t an export from one tool into the other. Both read the same OpenAPI file: Fern generates from it, Postman imports it. That leaves exactly one artifact to keep honest, which is the whole point:

openapi.yml
   |
   |-- fern generate ........ SDKs (nine languages) + CLI on npm
   |
   +-- import to Postman .... MCP Generator ..... MCP server

Put the specification under version control, run fern generate in CI whenever it changes, and re-import to Postman on the same trigger. One commit to openapi.yml, three surfaces rebuilt, nothing kept in sync by hand.

That’s what dissolved the choice I spent a month on. You stop picking a door and start shipping all of them, and each consumer takes the one that suits it.

Try this on your own API #

Take whichever API your team calls most and run the comparison yourself. An afternoon is enough:

  1. Write the smallest useful script against the official SDK. Note whether the types caught anything.
  2. Do the same work through the vendor’s CLI with structured output flags turned on, and compare response sizes.
  3. Connect the vendor’s MCP server. Before you send a message, check what share of your context window the tool definitions took. Scope the toolsets and check again.

Then ask who’s calling this in six months. Your own application code means generate an SDK. An agent working unattended means generate a CLI. A team behind one shared integration means stand up an MCP server. For most APIs worth the trouble the honest answer is all three, at which point the question isn’t which one you pick, it’s whether you’re generating them from the specification or maintaining them by hand.

I’d like to know where this falls apart for other people. The token arithmetic in particular moves every few months, and the numbers I measured in July may not survive the year.

Resources #

── more in #ai-agents 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/sdk-vs-cli-vs-mcp-a-…] indexed:0 read:12min 2026-09-17 ·