Listing tools for people
Version: 1.3.0 ยท Date: 2026-10-30
The listing helpers turn a provider's tools into a listing a person can
read: a terminal command (octools list) for anyone with the Python
package installed, and a small library API (summarize_provider and
render_listing in Python, summarizeProvider and renderListing in
TypeScript) for a library that wants the same listing inside its own CLI.
Both packages render the same text, byte for byte. Nothing here changes
the descriptor or the envelopes; it only reads them.
The Python package ships a demo provider, octools.example, so
octools list octools.example works wherever the package is installed: a
smoke test that the command runs, and a small conformant provider to read.
Its tools may change in any release, and it is not part of the public API.
The octools list command
The command ships with the Python package. The TypeScript package has no
command of its own; a TypeScript CLI renders the same listing through
renderListing (see
Wiring render_listing into your own CLI).
octools list SPEC [--access ACCESS] [-o PATH|-] [--output-format FORMAT] [--pretty | --no-pretty]
python -m octools list SPEC [...]
octools --version
| Option | Meaning |
|---|---|
SPEC |
The provider to list: a module, a module:attr object or a zero-argument factory. |
--access ACCESS |
Only list tools with this access, local or remote; omitted lists every tool. |
-o, --output-file PATH\|- |
Write the listing to PATH instead of stdout; - or omitted means stdout. The file is written as UTF-8 and replaced atomically. |
--output-format FORMAT |
table, md (markdown is accepted as an alias), txt or json. Omitted, it follows the -o extension, else table. |
--pretty, --no-pretty |
Indent json output or print it on one line. The default is indented when stdout is a terminal and compact when it is piped or -o is set. |
-v, -V, --version |
Print octools <version> and exit 0. |
With -o and no --output-format, the extension picks the format:
.md and .markdown write md, .json writes json, .txt writes
txt, and any other extension writes table. An explicit
--output-format always wins. An -o whose directory does not exist or
is not writable, or that names a directory, is a usage error before
anything is listed. Option names are never abbreviated: --output is an
unknown option, not a short form of --output-file.
SPEC names the provider to list, as package.module or
package.module:attr (attr may itself be dotted, a.b). The working
directory is importable, as it is under python -m, so a local module
lists without installing it. Resolution:
- Import the module.
- With no
:attr, the module itself is the candidate target; with:attr, resolve that attribute on it. - A target that already satisfies
ToolProvider(it has anall_toolsmethod) is used as-is. - Otherwise, a callable target is called with no arguments, and its
return value must satisfy
ToolProvider. - Anything else is an error.
- The provider's
all_tools()is called once, and every item it returns must be anOCTool.
Exit codes and errors
| Code | Meaning |
|---|---|
0 |
The listing was printed or written. |
1 |
The listing could not be read or written: a factory raised or returned a non-provider, all_tools() raised or returned a non-OCTool, or the output file could not be written. |
2 |
Usage error: an unknown option or value, an unusable -o path, or a SPEC whose module does not import, whose attribute is missing, or that names neither a provider nor a callable. |
130 |
Interrupted. |
stdout carries the listing or nothing: on any failure it is empty. A
usage error prints the command's usage and one
octools list: error: <message> line to stderr; any other failure prints
one error: <message> line:
$ octools list no_such_tools
usage: octools list [-h] [--access ACCESS] [-o PATH|-]
[--output-format FORMAT] [--pretty | --no-pretty]
SPEC
octools list: error: cannot import 'no_such_tools': No module named 'no_such_tools'
An all_tools() that raises reads
'<SPEC>': all_tools raised <Type>: <message>. A SPEC that names a
module-level all_tools function is called as a factory, and the tuple it
returns is not a provider; the error names the fix:
$ octools list octools.example:all_tools
error: 'octools.example:all_tools': factory returned tuple, not a tool provider; name the module 'octools.example' to use its all_tools
When the output format is json, given or inferred from -o, stderr
instead ends with exactly one JSON object on one line. type is usage
for exit 2 and api_failure for exit 1; reason, status_code and
details are always null, and hint carries the fix when there is one:
$ octools list octools.example:all_tools --output-format json
{"error": {"type": "api_failure", "exit_code": 1, "message": "'octools.example:all_tools': factory returned tuple, not a tool provider", "reason": null, "status_code": null, "details": null, "hint": "name the module 'octools.example' to use its all_tools"}}
The machine contract is --output-format json: the listing on stdout,
and nothing else there. Pipe it straight into jq:
octools list SPEC --output-format json | jq '.[].name'
Providers that need a client
A provider reachable as a module with a module-level all_tools()
works directly. A provider whose tools need a live client or
configuration to construct needs a client-less, zero-argument factory
function to be listed this way - one that builds the tools without
reaching a real system, for example by passing None as the injected
dependency.
Formats
table, md and txt are for a person reading a terminal or a chat
reply. json is the machine-readable form, for a script or another tool
to parse: a plain list of dicts, one per tool, in the same key order every
time.
The examples below list the demo provider, octools.example, with four
tools: echo_records (reads input back as records, one page at a
time), echo (its deprecated alias), block_sizes (address block sizes)
and render_badge (writes a badge image through an injected sink).
table
One line per tool, columns padded to width with a two-space gutter, no
header. mode is destructive when the tool's destructive hint is set,
write when it is not read-only, otherwise read. A deprecated tool
appends [deprecated; use <alias_of>], or [deprecated] when it names no
replacement.
$ octools list octools.example
echo_records local read Return each input string as one record with its position.
echo local read Deprecated alias of echo_records kept for one major version. [deprecated; use echo_records]
block_sizes local read Return each CIDR prefix normalized, with its length and address count.
render_badge local read Render a two-part SVG badge and write it through the injected sink.
md
A GitHub-flavored Markdown table over the same rows, with columns name,
access, mode, result_kind, pages, deprecated, summary. pages
is yes when the tool takes a cursor input, the paging convention in
Descriptor and envelopes.
$ octools list octools.example --output-format md
| name | access | mode | result_kind | pages | deprecated | summary |
| ------------ | ------ | ---- | ----------- | ----- | ---------- | ---------------------------------------------------------------------- |
| echo_records | local | read | records | yes | no | Return each input string as one record with its position. |
| echo | local | read | records | yes | yes | Deprecated alias of echo_records kept for one major version. |
| block_sizes | local | read | records | no | no | Return each CIDR prefix normalized, with its length and address count. |
| render_badge | local | read | artifact | no | no | Render a two-part SVG badge and write it through the injected sink. |
-o tools.md writes the same table to tools.md.
txt
One block per tool, blocks separated by a blank line: the name (and
- <title> when the tool has one), the full description wrapped at 88
columns, an access / target / result line, a hints line (ending
in pages when the tool takes a cursor input), a deprecated line
when the tool is deprecated, and the tool's inputs as plain aligned
columns of name, type, required and description (or
inputs: none). The columns are text for a terminal, not Markdown, so a
type such as string|null prints as is. The first two blocks:
$ octools list octools.example --output-format txt --access local
echo_records - Echo strings as records
Return each input string as one record with its position. Use it to check that a tool
round-trip works before calling anything real. Returns a records envelope; when limit
cut the list, truncated is true and next_cursor fetches the next page.
access: local target: - result: records
hints: read-only, idempotent, pages
inputs:
name type required description
values array[string] yes Strings to echo back, one record each.
limit integer no Maximum records to return, 1 to 100; the tool enforces it.
cursor string|null no next_cursor from a previous page; omit for the first page.
echo
Deprecated alias of echo_records kept for one major version. Call echo_records
instead.
access: local target: - result: records
hints: read-only, idempotent, pages
deprecated: use echo_records
inputs:
name type required description
values array[string] yes Strings to echo back, one record each.
limit integer no Maximum records to return, 1 to 100; the tool enforces it.
cursor string|null no next_cursor from a previous page; omit for the first page.
...
json
json.dumps([summary.as_row() for summary in summaries], indent=2) with
--pretty (the default on a terminal), or on one line with
separators=(",", ":") otherwise; one flat dict per tool (the first shown
here):
$ octools list octools.example --output-format json --pretty
[
{
"name": "echo_records",
"title": "Echo strings as records",
"summary": "Return each input string as one record with its position.",
"description": "Return each input string as one record with its position. Use it to check that a tool round-trip works before calling anything real. Returns a records envelope; when limit cut the list, truncated is true and next_cursor fetches the next page.",
"access": "local",
"target": null,
"result_kind": "records",
"read_only": true,
"destructive": false,
"idempotent": true,
"pages": true,
"deprecated": false,
"alias_of": null,
"inputs": [
{
"name": "values",
"type": "array[string]",
"required": true,
"description": "Strings to echo back, one record each."
},
{
"name": "limit",
"type": "integer",
"required": false,
"description": "Maximum records to return, 1 to 100; the tool enforces it."
},
{
"name": "cursor",
"type": "string|null",
"required": false,
"description": "next_cursor from a previous page; omit for the first page."
}
]
},
...
]
Library API
Both packages export the pieces above so a producer's own CLI can render
the same listing without shelling out to octools list. The TypeScript
names are camelCase: summarizeTool, summarizeProvider,
renderMarkdownTable, renderListing.
ToolSummary and InputSummary
summarize_tool(tool) returns a ToolSummary: name, title,
summary (the first sentence of description, or the whole description
when no sentence boundary is found), description, access, target,
result_kind, read_only, destructive, idempotent, pages (true
when the input schema's properties include cursor), deprecated,
alias_of (the tool's meta["alias_of"] when it is a non-empty string,
else None), and inputs, a tuple of InputSummary. In TypeScript the
ToolSummary properties are camelCase (resultKind, readOnly,
aliasOf) and inputs is a readonly array.
Each InputSummary describes one property of the tool's input schema:
name, type (rendered from the property's JSON Schema, see below),
required, and description (the property's own description when it
is a string, else None). Inputs are read in the schema's own property
order.
ToolSummary.as_row() (asRow()) returns a flat, JSON-ready dict in a
fixed key
order - name, title, summary, description, access, target,
result_kind, read_only, destructive, idempotent, pages,
deprecated, alias_of, inputs (each input as a dict of name,
type, required, description) - the shape the json format
serializes. The row keys are the same snake_case keys in both packages.
Type rendering never raises, for any input mapping:
| Property shape | Rendered type |
|---|---|
"type": "string" |
string |
"type" is a list |
member types joined with \| (string\|null) |
"type": "array" with "items" |
array[<items type>] |
"enum" present, with or without "type" |
enum(a\|b\|c); past 40 characters, the values that fit then \|... |
"const" |
const(<value>) |
"$ref" |
the last path segment (#/$defs/Mode -> Mode) |
"anyOf" or "oneOf" |
member types joined with \| (enum(a\|b)\|null) |
| anything else | any |
enum and const take precedence over the other keys, since their values
are what a reader needs. A string value renders bare and any other value
as JSON (enum(null|true|1.5)).
summarize_provider
Summarizes every tool provider.all_tools() returns, in that order,
filtered to access ("local" or "remote") when given.
render_markdown_table and render_listing
function renderMarkdownTable(
rows: readonly Readonly<Record<string, unknown>>[],
columns: readonly string[],
): string;
function renderListing(
provider: ListedToolProvider,
options?: RenderListingOptions, // { style?, access?, pretty? }
): string;
ListedToolProvider is any object whose allTools() lists tools;
every ToolProvider is one.
render_markdown_table pads a GitHub-flavored table over any rows and
column names: a bool cell renders as yes / no, None or a missing
key as an empty cell, anything else via str(); a literal | in a cell
is escaped as \| and a newline becomes a space. With no rows it renders
the header and divider only. The result always ends with a newline.
render_listing is the one call behind the octools list command:
given a provider and a style (one of the formats above), it summarizes
and renders in one step. An unknown style raises ValueError (a
RangeError in TypeScript) naming the valid ones, LISTING_STYLES ("table", "md", "txt", "json", also
exported as the ListingStyle literal); markdown is a CLI alias only.
pretty applies to json alone: indented when true (the default),
compact on one line when false. The result ends with a newline, except
that an empty listing renders as an empty string for table and txt;
md then renders the header and divider only, and json renders
[]\n.
Wiring render_listing into your own CLI
A library that already ships a CLI can add its own tools list command
by delegating straight to render_listing (renderListing), instead of
building rows by hand:
import argparse
from octools import LISTING_STYLES, render_listing
from octools import example as my_tools
def cmd_tools_list(args: argparse.Namespace) -> None:
text = render_listing(my_tools, style=args.output_format, access=args.access)
print(text, end="")
parser = argparse.ArgumentParser()
subcommands = parser.add_subparsers(dest="command")
tools_list = subcommands.add_parser("tools-list")
tools_list.add_argument(
"--output-format",
metavar="FORMAT",
choices=LISTING_STYLES,
default="table",
help="Render the listing in this format.",
)
tools_list.add_argument(
"--access",
metavar="ACCESS",
choices=["local", "remote"],
help="Only list tools with this access.",
)
tools_list.set_defaults(func=cmd_tools_list)
import { parseArgs } from "node:util";
import {
ACCESS_KINDS,
LISTING_STYLES,
type Access,
type ListingStyle,
renderListing,
} from "@opscogs/octools";
import * as myTools from "./my_tools.js";
function oneOf<T extends string>(
value: string | undefined,
allowed: readonly T[],
option: string,
): T | undefined {
if (value !== undefined && !(allowed as readonly string[]).includes(value)) {
throw new RangeError(`${option} must be one of ${allowed.join(", ")}`);
}
return value as T | undefined;
}
export function cmdToolsList(args: string[]): void {
const { values } = parseArgs({
args,
options: {
"output-format": { type: "string", default: "table" },
access: { type: "string" },
},
});
const style: ListingStyle | undefined = oneOf(
values["output-format"],
LISTING_STYLES,
"--output-format",
);
const access: Access | undefined = oneOf(
values.access,
ACCESS_KINDS,
"--access",
);
process.stdout.write(renderListing(myTools, { style, access }));
}
Every producer's tools list command then supports the same four formats
with no per-library formatting code, and stays in sync with the
descriptor whenever a tool's description, hints or inputs change.