Skip to main content

Overview

This tutorial walks you through creating a simple CLI that greets users. You’ll learn:
  • How to create a CLI with Cli.create()
  • How to define arguments and options with Zod schemas
  • How to add multiple commands
  • How to enable agent discovery
  • How to test your CLI with an agent
1

Create a simple CLI

Create a new file greet.ts with a basic single-command CLI:
greet.ts
import { Cli, z } from 'incur'

const cli = Cli.create('greet', {
  description: 'A greeting CLI',
  args: z.object({
    name: z.string().describe('Name to greet'),
  }),
  run(c) {
    return { message: `hello ${c.args.name}` }
  },
})

cli.serve()
Key points: Cli.create() takes a name and configuration. The args field defines positional arguments with Zod schemas. The run() function receives typed context with c.args.name, and the return value is automatically wrapped and formatted.
2

Run your CLI

Execute your CLI:
npx tsx greet.ts world
You’ll see:
message: hello world
This is TOON format (Token-Optimized Object Notation) - more readable than JSON and uses 60% fewer tokens.
Try adding --json to see JSON output: npx tsx greet.ts world --json
Check the built-in help:
npx tsx greet.ts --help
Output:
greet – A greeting CLI

Usage: greet <name>

Arguments:
  name  Name to greet

Built-in Commands:
  completions  Generate shell completion script
  mcp add      Register as an MCP server
  skills add   Sync skill files to your agent

Global Options:
  --format <toon|json|yaml|md|jsonl>  Output format
  --help                              Show help
  --llms                              Print LLM-readable manifest
  --mcp                               Start as MCP stdio server
  --verbose                           Show full output envelope
  --version                           Show version
3

Add options

Extend your CLI with options for more control:
greet.ts
import { Cli, z } from 'incur'

const cli = Cli.create('greet', {
  description: 'A greeting CLI',
  args: z.object({
    name: z.string().describe('Name to greet'),
  }),
  options: z.object({
    loud: z.boolean().optional().describe('Shout the greeting'),
    emoji: z.boolean().optional().describe('Add emoji'),
  }),
  run(c) {
    let message = `hello ${c.args.name}`
    if (c.options.loud) message = message.toUpperCase()
    if (c.options.emoji) message = `👋 ${message}`
    return { message }
  },
})

cli.serve()
Try it:
npx tsx greet.ts world --loud --emoji
Output:
message: 👋 HELLO WORLD
4

Add multiple commands

Convert your CLI to support multiple commands:
greet.ts
import { Cli, z } from 'incur'

const cli = Cli.create('greet', {
  version: '1.0.0',
  description: 'A greeting CLI',
})

cli.command('hello', {
  description: 'Say hello',
  args: z.object({
    name: z.string().describe('Name to greet'),
  }),
  options: z.object({
    loud: z.boolean().optional().describe('Shout the greeting'),
  }),
  run(c) {
    const message = `hello ${c.args.name}`
    return { message: c.options.loud ? message.toUpperCase() : message }
  },
})

cli.command('goodbye', {
  description: 'Say goodbye',
  args: z.object({
    name: z.string().describe('Name to say goodbye to'),
  }),
  run(c) {
    return { message: `goodbye ${c.args.name}` }
  },
})

cli.serve()

export default cli
Now you can:
npx tsx greet.ts hello alice
# → message: hello alice

npx tsx greet.ts goodbye bob
# → message: goodbye bob

npx tsx greet.ts --help
# Shows all available commands
The export default cli is important for agent discovery and type generation.
5

Enable agent discovery

Make your CLI discoverable by AI agents using Skills:
npx tsx greet.ts skills add
This generates skill files and installs them globally so agents can discover your CLI automatically.Output:
  ✓ greet  A greeting CLI

1 skill synced

Run `greet --help` to see the full command reference.
Skills are lightweight Markdown files that describe your CLI’s commands, arguments, and options. They’re automatically kept in sync with your code.
Alternatively, register as an MCP server:
npx tsx greet.ts mcp add
This adds your CLI to your agent’s MCP configuration.
6

Test with an agent

Now your agent can discover and use your CLI. Try asking:
"Use the greet CLI to say hello to Alice"
The agent will automatically:
  1. Discover your CLI via Skills or MCP
  2. Read the available commands and their schemas
  3. Execute greet hello alice
  4. Parse the TOON output
You can also test the machine-readable manifest:
npx tsx greet.ts --llms
This outputs Markdown documentation that agents can read:
# greet hello

Say hello

## Arguments

| Name   | Type     | Required | Description    |
| ------ | -------- | -------- | -------------- |
| `name` | `string` | yes      | Name to greet  |

## Options

| Flag     | Type      | Required | Description         |
| -------- | --------- | -------- | ------------------- |
| `--loud` | `boolean` | no       | Shout the greeting  |

# greet goodbye

Say goodbye

## Arguments

| Name   | Type     | Required | Description              |
| ------ | -------- | -------- | ------------------------ |
| `name` | `string` | yes      | Name to say goodbye to   |

What You’ve Learned

CLI Creation

How to create CLIs with Cli.create() and add commands with .command()

Type Safety

Using Zod schemas for arguments and options with automatic type inference

Output Formats

TOON as the default token-efficient format, with JSON, YAML, and more available

Agent Discovery

Enabling automatic discovery via skills add and mcp add

Next Steps

Commands & Arguments

Deep dive into command patterns, schemas, and validation

Output & Formats

Learn about output envelopes, CTAs, and format options

Agent Integration

Configure Skills, MCP, and customize agent behavior

Complete Example

Here’s the final CLI from this tutorial:
greet.ts
import { Cli, z } from 'incur'

const cli = Cli.create('greet', {
  version: '1.0.0',
  description: 'A greeting CLI',
})

cli.command('hello', {
  description: 'Say hello',
  args: z.object({
    name: z.string().describe('Name to greet'),
  }),
  options: z.object({
    loud: z.boolean().optional().describe('Shout the greeting'),
  }),
  run(c) {
    const message = `hello ${c.args.name}`
    return { message: c.options.loud ? message.toUpperCase() : message }
  },
})

cli.command('goodbye', {
  description: 'Say goodbye',
  args: z.object({
    name: z.string().describe('Name to say goodbye to'),
  }),
  run(c) {
    return { message: `goodbye ${c.args.name}` }
  },
})

cli.serve()

export default cli
Want to see a more complete example? Check out the npm CLI example in the incur repository.

Build docs developers (and LLMs) love