Cutting the cost of every answer
Why the CLI costs an AI agent a fraction of what a tool server does — discovery on demand, and response pruning that happens before anything is read.
Every token an agent reads is paid for, and slows the answer down. Two habits do almost all the work of keeping that cost low, and both are structural — you get them from how the CLI is shaped, not from being clever at the prompt.
Habit one: discover what you need, when you need it
An MCP client loads every tool definition into the model's context before it reads your request. All of them, every session, whether the session ends up sending a message or reading a profile or doing nothing at all.
Here is what that costs on this surface, measured against the CLI's own schema
output for the same 87 commands:
| What you load | Size | Roughly |
|---|---|---|
| Full parameter schemas for all 87 commands | 145 KB | 36,000 tokens |
The commands catalogue — name, group, verb, summary | 10.7 KB | 2,700 tokens |
One command's full schema (search-people) | 7.5 KB | 1,900 tokens |
An agent that browses the catalogue and then reads one schema spends about 4,600 tokens. The upfront-everything approach spends 36,000 before the user's first sentence is processed — roughly eight times as much, on every session, most of it about commands that session will never touch.
connectsafely commands # 2,700 tokens: everything, one line each
connectsafely commands --search invitation # far less: only what matches
connectsafely commands --kind write # only things that change your account
connectsafely schema send-connection-request # 1 command, in fullcommands needs no API key, works offline, and reads the specification bundled in
the binary. An agent can plan its work before credentials exist.
Habit two: ask the API for less
The larger saving is on the way back. A search that returns 25 people returns everything the API knows about each of them — ids, URNs, image references, degree metadata, tracking fields. If all you needed was a name and a headline, the rest is paid for and thrown away.
--fields prunes the response inside the CLI process, before it is printed. The
data you did not ask for is never read by the model, so it is never charged for:
# everything the API knows about 25 people
connectsafely search-people --keywords "VP Marketing" --count 25
# a name and a headline
connectsafely search-people --keywords "VP Marketing" --count 25 \
--fields 'people.firstName,people.lastName,people.headline'The mask is a comma-separated list of dotted paths. It descends into nested objects and applies across arrays automatically:
--fields 'people.firstName' # one field from every row
--fields 'people.firstName,people.headline' # several
--fields 'conversations.participants.name' # nested, through an array
--fields 'people[].firstName' # [] is accepted and ignored
--fields 'conversations.participants' # a bare parent keeps its whole subtreeEnvelope keys — success, error, message, and pagination cursors — are always
kept, whatever mask you write. A failed call must never look like an empty result
just because the mask did not mention the error field.
--max-items caps every array in the payload, and says so when it cuts one:
connectsafely search-people --keywords "CTO" --count 50 --max-items 5{
"success": true,
"people": [ … 5 rows … ],
"truncated": { "maxItems": 5, "originalLength": 50 }
}That marker matters more than it looks. Without it, a capped list is indistinguishable from a short one, and an agent concludes it has seen everything when it has seen a tenth.
Pick the cheapest shape for the job
--output changes the rendering, and rendering is not free either.
ndjson — one JSON object per line. The cheapest way to hand rows to a script,
and the natural input format for batch:
connectsafely search-people --keywords "CTO" --count 50 \
--fields 'people.profileId,people.firstName' \
--output ndjson > prospects.ndjsonmarkdown — a table. Language models read Markdown structure natively, so 20
rows as a table cost noticeably less than the same rows as pretty-printed JSON, and
stay just as unambiguous.
json — the default for any non-terminal caller. Indented, complete,
round-trippable.
pretty — aligned columns for a human. The default when the CLI detects a real
terminal and no agent runtime.
You rarely need to pass --output at all. A human at a terminal gets pretty; a
pipe, a CI job or a known agent runtime gets json. Set CONNECTSAFELY_OUTPUT to
change the default for a whole session.
Keep data out of context entirely
The strongest version of this idea: when an intermediate result only exists to feed the next step, it does not need to pass through the model at all.
# 50 prospects go to a file, not into context
connectsafely search-people --keywords "VP Marketing" --count 50 \
--fields 'people.profileId,people.firstName' \
--output ndjson > prospects.ndjson
# a shell one-liner turns them into calls — still no tokens spent on the rows
jq -c '{command:"send-connection-request",args:{profileId:.profileId},ref:.firstName}' \
prospects.ndjson > invites.ndjson
# only the summary line comes back to the model
connectsafely batch invites.ndjson | jq '.summary'Fifty profiles were sourced, transformed and acted on, and the model read a summary line. This is the thing a shell can do that a tool call cannot: intermediate results stay on disk.
A rule of thumb
Add
--fieldsand--max-itemsto every read. Usendjsonand a file whenever the output feeds the next command rather than a person.
Both are in the skill file, so an agent that has read it will do this without being asked each time.
Running the CLI from an AI agent
What changes when an AI agent drives your LinkedIn account through a CLI instead of a tool server — fewer wrong actions, less context burned, and failures the agent can actually recover from.
Not sending the wrong thing
Rehearse writes before they happen, catch bad arguments before they leave the machine, and make destructive actions impossible to run by accident.
