skip to content

Building porin: a library for agent-facing CLIs


Last week I wrote about seven patterns for agent-facing CLIs. This week I turned them into a library.

porin is a zero-dependency Python library that handles the output side of agent-facing CLIs: JSON envelopes, command tree discovery, NDJSON streaming, and semantic exit codes. It works with any CLI framework — Click, Typer, cyclopts, argparse. The name comes from cell biology. Porins are barrel-shaped proteins in bacterial outer membranes that form structured channels for selective passage. The library is the structured channel between your CLI and the agent invoking it.

The seven patterns sound simple when described. Implementing them revealed three things the patterns do not say. Discovery needs two tiers. Bare invocation returns the command tree, but how much detail? Full schemas with return types, error codes, and examples waste tokens when the agent just needs to know what commands exist. porin splits this into a lightweight dictionary of names and params, which is what you emit on bare invocation, and a full schema with everything, which is what the schema command returns when the agent needs detail.

Annotations are safety-critical. Anthropic’s MCP spec defines four behavioural hints: read-only, destructive, idempotent, and open-world. Agents use these to decide whether to auto-execute or ask for confirmation. Without them, every tool invocation requires the same level of caution. With them, readonly operations can run unattended while destructive ones get human review. porin lets you declare these per command and they flow through to both the command tree and the MCP bridge.

Pagination is not optional. Dumping five hundred results into an agent’s context window is actively harmful. Anthropic restricts Claude Code tool responses to 25,000 tokens by default. porin’s paginated response returns items with a has_more flag and a cursor — the agent decides when to stop fetching.

The most interesting addition is the MCP bridge. One function call turns a porin CLI into a stdio MCP server. The bridge maps porin annotations to MCP hints automatically. The command tree becomes the tool schema. The dispatch function is the handler. No MCP boilerplate. This matters because the CLI-versus-MCP decision should not be either/or. Your CLI is the durable artifact. The MCP server is an adapter. porin makes the adapter free.

Zero dependencies is a feature, not a constraint. The core library has no deps. The MCP bridge is an optional extra. This means the core works everywhere, including constrained environments where you cannot install heavy MCP dependency trees. Agent-facing is a layer, not a framework. porin does not replace Click or Typer. It is the output layer that sits underneath. Your CLI framework handles argument parsing, subcommands, and help text. porin handles what the agent sees in stdout. The gap between patterns and code was smaller than expected. The entire core is 130 lines.

porin on PyPI | source on GitHub