Skip to content

Printing Press

Printing Press is a registry pattern for small purpose-built CLIs — ESPN scores, weather, currency, anything that fits one binary + a registry.json. Z.E.N.'s pp connector adapter reads a Printing Press registry file at boot, registers each CLI's documented commands as pp.<registry>.<cli>.<command> tools, and they're immediately available to tool: and agent: nodes.

This is for the case where you have a collection of small CLIs that share a common registry shape, not one CLI you want to wire in directly (use the local_cli connector path for that).

Where Z.E.N. looks for the registry

The adapter checks (in order):

  1. ~/.printing-press/registry.json
  2. ~/.zen/printing-press/registry.json
  3. Path set via ZEN_PP_REGISTRY env var

The first one found wins. If none exist, the connector reports pp as toolCount: 0 and stays silent. No CLIs registered, no errors.

registry.json shape

json
{
  "version": 1,
  "clis": [
    {
      "id": "espn",
      "displayName": "ESPN",
      "description": "Scores, schedules, standings.",
      "bin": "/usr/local/bin/pp-espn",
      "commands": [
        {
          "id": "scoreboard",
          "displayName": "Today's scoreboard",
          "args": ["scoreboard", "--json"],
          "outputFormat": "json"
        },
        {
          "id": "team",
          "displayName": "Team details",
          "args": ["team"],
          "outputFormat": "json",
          "schema": {
            "type": "object",
            "properties": {
              "name": { "type": "string", "description": "Team abbreviation, e.g. SF" }
            },
            "required": ["name"]
          }
        }
      ]
    }
  ]
}

Each CLI's commands[] becomes one tool. The tool id is pp.espn.scoreboard, pp.espn.team, and so on. The bin is the absolute path; args is the always-on prefix; user's tool_args gets appended.

Verify Z.E.N. picked it up

bash
curl -s http://localhost:3090/api/connectors | jq '.connectors[] | select(.id == "pp")'
curl -s http://localhost:3090/api/tools | jq '.tools[] | select(.connectorId == "pp") | .id'

Settings → Connectors shows pp with local_cli chip and the total tool count summed across every CLI in the registry.

Use it from a workflow

yaml
- id: today-games
  tool: pp.espn.scoreboard

- id: lakers
  tool: pp.espn.team
  tool_args:
    name: LAL

The tool_args shape matches whatever schema the command declared in registry.json. The picker in the workflow editor reads the schema and shows the form inline.

Add a new CLI to your registry

Edit your registry.json, add the CLI entry, restart Z.E.N. The connector re-parses on boot. No need to register each CLI separately.

For a sample registry covering ESPN, OpenWeather, exchange rates, and a few others, see mvanhorn/printing-press.

When this fits

Printing Press fits when you want a curated registry of small consumer-grade CLIs you (or your team) maintain together. One JSON file, many CLIs, all exposed to Z.E.N. as tools.

When it doesn't fit:

  • One single CLI → write a local_cli connector manifest instead. Simpler.
  • An MCP server → use mcporter. Different protocol, different registry shape.
  • A REST API → wrap it with an http-source-kind connector manifest.

Gotchas

  • The bin path must be absolute. Same daemon-PATH-stripping issue as gws/mcporter. If your CLIs live in ~/.local/bin, write the absolute path in the registry.
  • registry.json validation is light. The adapter parses what it can and skips entries with missing required fields silently. If a CLI you expected isn't registered, run bun run dev and watch the pp.bootstrap log lines — they'll tell you which entries got dropped and why.
  • Reload requires a daemon restart. Editing registry.json doesn't hot-reload. zen serve to pick up changes.
  • No auth model. Printing Press registry doesn't declare auth — the assumption is these CLIs read their own credentials from env. Set the env vars in ~/.zen/.env so the daemon forwards them.

Reference

AI that follows a recipe, not a conversation.