There’s a question we weren’t asking ourselves a year ago, but now it pops up with every tool we touch: Can an agent use this? Not a developer with the documentation open, but a model that receives a task, decides which command to execute, reads the output, and moves on. At Transparent Edge, we have a command-line tool, te-api, that wraps our entire API. It worked well for humans. For an agent, it was a minefield. Today, we’ll explain what we’ve changed, what we’ve focused on, and what you can do with it.
What Cloudflare taught us
A few days ago, Cloudflare unveiled cf, its new CLI, with a phrase that stuck in our minds: they built it “from the ground up with agents in mind.” It’s not a tool for people to which you then add --json; it’s the other way around.
We are less interested in the Workers and TypeScript configuration sections of the post, as they are not applicable to our case, and much more interested in three ideas about how a CLI should behave when invoked by a model:
- JSON by default. In their words, “pretty printed for humans and condensed for agents, to save as much context as possible.” The format is decided by the person on the other end, instead of an option you never remember to set.
- Integrated discovery. With more than 3,000 operations, an agent cannot read the help of all of them. So cf cli search receives a natural language question and returns candidate commands based on the API description and its parameters. And the tool itself tells the agent the first time it asks for
--help. - A guide for the agent, not the developer. An AGENTS.md file (thanks Claude for finally sticking to the standard) that explains how to use the tool, not how to contribute to it.
What caught our attention the most is that all of this is generated from the OpenAPI schema, the same one that powers its documentation and SDKs. And that made us smile a little, because te-api has been doing exactly that for some time now. Thanks, Peter Steinberger, for showing us the way 🙂
Where did we start from?
te-api is a Python CLI that automatically generates itself from the OpenAPI schema that serves our API. The first time you run it, it downloads the schema using your credentials (in your .env file) and builds a command for each operation you have access to: over 350 in the full version. When the API changes, the tool detects and regenerates itself. No one should have to manually maintain a list of commands, so we’ve tried to make it as easy and maintainable as possible.
The structure was already one that an agent appreciates: te-api <module> <verb> <resource> with the verbs standardized to get, create, update and delete, and a variant te-api-ro that only exposes reads, for when you prefer that an agent cannot break anything.
The problem was with what was displayed on the screen. The tool was speaking to a person sitting in front of a terminal:
- If a call failed, it would print “Error: …” to
stdoutand terminate with code 0. For a script, that’s a success. For an agent, it’s text they have to interpret, and it’s easy to make a mistake. - All the JSON was indented, every time. Easy to read, but expensive in tokens when the response is thousands of lines long.
- The commands that sent data had a
--json-bodyoption whose help literally stated, “JSON string for request body.” The schema for that body was in the OpenAPI, but unused. The agent knew something had to be sent, but not what. It’s worth noting that this wasn’t very user-friendly, so it needed to be fixed. - To find a command, you had to navigate through
--helplevel by level. With 350 commands, that’s a lot of calls and a lot of context wasted before doing anything useful.
None of this was a design flaw when it was written. It’s simply that the user we had in mind was different.
A machine-readable exit contract
The first thing we did was the least flashy but most important: define, in one place, what outputs what and what each output code means. Previously, each of the 350 generated commands carried its own copy of the block that made the request and displayed the result. Now they all delegate to a common module, and the agreement is this:
stdoutcarries the API response and nothing else. Ifstdoutis a terminal, the JSON is output indented. If it is a pipe, it comes out compact. It’s the same idea as Cloudflare: the format is decided by who listens.--output pretty|compactforces one or the other when necessary.- Errors are a JSON object in
stderr, containing the failure type, HTTP status, URL, and the body returned by the API. - The exit code distinguishes the type of failure: 0 all good, 1 the API responded with an error, 2 the command line was wrong, 3 the API could not be reached or the token could not be obtained.
In practice, a failed call looks like this:
$ te-api inventory get node 999999999
{"error":{"type":"api","message":"HTTP 404 Not Found","status":404,"url":"https://api.transparentcdn.com/v1/inventory/node/999999999/","body":{"code":"not_found","message":"Not found."}}}
$ echo $?
1
And one that works, chained with jq, focuses on what matters:
$ te-api companies get current-user | jq .id
10
It seems like a small thing. But an agent that can trust the output code and parse stderr doesn’t need to guess anything, and one that receives compact JSON uses half the context on each response. Everything else relies on this.
So the agent knows what to send
The second change affects the “JSON string for request body.” The schema for each request body was in OpenAPI, almost always hidden behind a reference to a shared component. The generator now resolves those references, folds the compositions (allOf, oneOf), and builds the help from the actual schema: each property with its type, whether it’s required, and its description. Properties that the API marks as read-only are omitted because they are received, never sent.
$ te-api companies create alerts --help
--json-body TEXT JSON request body. JSON object with keys:
threshold(integer [required]) company(integer [required])
service(integer) active(boolean)
reactions(array of object [required])
In addition to --json-body, all these commands now accept --body-file, with - to read from standard input. Writing JSON on the command line is a breeding ground for quoting errors, and an agent that constructs the command programmatically makes them just as easily—or even more so—than a person. With a file, there’s no escaping it. And if the JSON arrives corrupted, by whatever route, the tool reports it as a usage error, with code 2, instead of dropping a Python trace.
Finding the command without reading 350 hints
Here we’re simply copying the idea from cf cli search, let’s not kid ourselves. Cloudflare has over 3,000 operations and needs a search index. We have a tenth of that, and something much simpler is enough.
The generator now writes, along with the commands, a catalog.json with a tab per command: name, summary, HTTP method, path, each parameter with its type, if mandatory and its possible values, and the body schema if it has one. On that catalog there are two new commands:
$ te-api search waf events --limit 2
[{"command":"statistics get waf","summary":"WAF statistics","method":"get","path":"/v2/statistics/{company_id}/waf/{temporality}/{request_type}/","score":3.0},
{"command":"statistics get waf-header-info","summary":"WAF header Details","method":"get","path":"/v1/statistics/{company_id}/waf/header_info/{waf_header_id}/","score":3.0}]
$ te-api describe statistics get waf
{"command":"statistics get waf","method":"get","path":"/v2/statistics/{company_id}/waf/{temporality}/{request_type}/","parameters":[{"name":"temporality","in":"path","argument":true,"required":true,"choices":["historic","analytic"]}, ...
➜ search scores based on word overlap between your query and the command name, its summary, its description, and its parameter names. The name is weighted higher than the summary, and the summary higher than the rest. There are no embeddings or dependencies, and it works for this size.
➜ describe describe is actually our favorite part. It returns the complete definition of a command as JSON, which is exactly what an agent needs to make a successful call the first time. It’s the equivalent of a tool definition on an MCP server, but for the entire API and without deploying anything. And if you give it just a prefix, te-api describe billing get lists what’s underneath.
The guide lives within the binary.
Cloudflare mentions an AGENTS.md file that accompanies the tool. We already had one, but it was for those who develop te-api, not for users. So we added a command, te-api agents, which prints a short guide designed for those who will be running the tool without supervision: the form of the commands, how to find them, the exit contract and error codes, how to set the company to work with using an environment variable instead of modifying shared state, and how authentication works.
The guide is included in the binary for a practical reason: it always describes the version you have installed, not the version that existed when it was documented. And with --skill it’s wrapped with the header that Claude Code expects, so installing it as a skill is just one line:
mkdir -p ~/.claude/skills/te-api
te-api agents --skill > ~/.claude/skills/te-api/SKILL.md
También copiamos el detalle de que la herramienta se presente sola. El --help raíz dice ahora, en tres líneas, que existen search, describe y agents, y que las respuestas son JSON en stdout y los errores JSON en stderr. Es la única ayuda que un agente lee seguro, así que es donde tiene que estar.
What we haven’t done, on purpose
It would be easy to read Cloudflare’s post and want to replicate it entirely, but we opted to give it our practical touch and adopt what is most useful.
➜ There’s no semantic search. With 350 commands, keyword search handles most cases, and when it fails, the full list of commands fits within the context of any current model. Adding embeds for this would be adding a dependency and a source of errors for little gain. If the API ever triples in size, we’ll review it. Problem for tomorrow’s team.
➜ There’s no typed configuration. The section of the post dedicated to cloudflare.config.ts solves a worker deployment problem that we don’t have. The only thing te-api needs to know is which company you’re working with, and that’s handled with an environment variable.
➜ We haven’t changed the command names. They come from the OpenAPI scheme, and the agents handle them without issue. Changing them now would break the scripts that already use them, for a purely aesthetic benefit for people with OCD.
The rule we followed was simple: each change had to eliminate a specific way an agent could make a mistake using the tool. If there wasn’t one, it wasn’t included.
Try it
te-api is open source, licensed under GPL-3.0, and available on GitHub. It is installed with uv and requires your Transparent Edge API OAuth2 credentials.
uv tool install git+https://github.com/TransparentEdge/te-api.git
export TRANSPARENT_CLIENT_ID="..."
export TRANSPARENT_CLIENT_SECRET="..."
te-api agents
The first run downloads your schema and generates the commands you have access to. From there, te-api search and te-api describe are the entry point, whether you’re typing or an agent is.
If you want to give an agent access without the risk of them modifying anything, install the same thing and use te-api-ro: the same tool, the same exit contract, read-only.
And if you test it and find a way your agent keeps getting stuck, open a ticket. That’s exactly the kind of problem we want you to tell us about.
FAQ
No, it complements it. An MCP server offers a curated set of tools with model-specific descriptions and is the best option for most common use cases. The CLI covers 100% of the API, including operations no one thought to expose, and requires no deployment or maintenance: the OpenAPI schema is the only source. In practice, an agent with access to a terminal and te-api descriptions can do anything the API allows.
With anyone who can run terminal commands. The output contract, search, describe, and agent guide are model-independent. The --skill option adds the header that Claude Code expects because that’s what we use, but the guide text serves the same purpose as system instructions for anyone else.
You don’t need to do anything. The commands and catalog are generated from the schema provided by the API itself, and the tool detects when the generated code doesn’t match the installed version. If you prefer to force it, te-api build regenerates everything in a few seconds.
Diego Suárez is Chief Technology Officer at Transparent Edge.
If you’re ever looking for Diego, you’ll likely find him where there’s an operating system involved—not taking the easy path, but always diving into the most complex alternative. His constant edge-walking gives him the versatility and vision needed to shape the tech strategy at the core of Transparent Edge. Always straddling development and systems, he still finds time to stay in touch with users—because ultimately, they’re who every tech company works for.
