@lifeprompt/acli
v0.7.3
Published
acli - Agent CLI protocol for AI agents on MCP
Maintainers
Readme
ACLI - Agent CLI
ACLI (Agent CLI) is a lightweight CLI protocol for AI agents built on top of MCP (Model Context Protocol).
Why ACLI?
Traditional MCP tool definitions require extensive schema for each tool, consuming valuable context window space. ACLI solves this by:
- Single Tool per Domain: One MCP tool (e.g.,
math,calendar) handles related commands - Dynamic Discovery: Agents learn commands via
helpandschema - Shell-less Security: No shell execution, preventing injection attacks
- Type-safe Arguments: Zod-based validation with full TypeScript inference
- CLI & MCP Dual Support: Use as MCP tool or standalone CLI
Installation
npm install @lifeprompt/acli zod
# or
pnpm add @lifeprompt/acli zodhttps://github.com/user-attachments/assets/c4b2a395-446c-4178-b552-9868ee40403c
Quick Start
MCP Server Integration
import { z } from "zod"
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { registerAcli, defineCommand, arg } from "@lifeprompt/acli"
// Use defineCommand() for full type inference in handlers
const add = defineCommand({
description: "Add two numbers",
args: {
a: arg(z.coerce.number(), { positional: 0 }),
b: arg(z.coerce.number(), { positional: 1 }),
},
handler: async ({ a, b }) => ({ result: a + b }), // a, b are inferred as number
})
const multiply = defineCommand({
description: "Multiply two numbers",
args: {
a: arg(z.coerce.number(), { positional: 0 }),
b: arg(z.coerce.number(), { positional: 1 }),
},
handler: async ({ a, b }) => ({ result: a * b }),
})
const commands = { add, multiply }
const server = new McpServer({ name: "my-server", version: "1.0.0" })
// Register as "math" tool
registerAcli(server, "math", commands, "Mathematical operations.")
// → Tool description: "Mathematical operations. Commands: add, multiply. Run 'help' for details."How AI Agents Call ACLI Tools
Once registered, AI agents (like Claude) call the tool with a command string:
// Tool call from AI agent
{
"name": "math",
"arguments": {
"command": "add 10 20"
}
}
// Response
{
"content": [{ "type": "text", "text": "{\"result\":30}" }]
}// Discovery - agents can explore available commands
{ "name": "math", "arguments": { "command": "help" } }
{ "name": "math", "arguments": { "command": "help add" } }
{ "name": "math", "arguments": { "command": "schema" } }Standalone CLI
#!/usr/bin/env node
import { z } from "zod"
import { defineCommand, runCli, arg } from "@lifeprompt/acli"
const greet = defineCommand({
description: "Say hello",
args: {
name: arg(z.string(), { positional: 0 }),
},
handler: async ({ name }) => ({ message: `Hello, ${name}!` }), // name is inferred as string
})
runCli({ commands: { greet } })node my-cli.mjs greet World
# → { "message": "Hello, World!" }Interactive REPL
Export commands from a file and explore them interactively — like dropping into a Docker container:
// tools.ts
import { z } from "zod"
import { defineCommand, arg } from "@lifeprompt/acli"
export const add = defineCommand({
description: "Add two numbers",
args: {
a: arg(z.number(), { positional: 0 }),
b: arg(z.number(), { positional: 1 }),
},
handler: async ({ a, b }) => ({ result: a + b }),
})
export const greet = defineCommand({
description: "Say hello",
args: {
name: arg(z.string(), { positional: 0 }),
shout: arg(z.boolean().default(false)),
},
handler: async ({ name, shout = false }) => {
const msg = `Hello, ${name}!`
return { message: shout ? msg.toUpperCase() : msg }
},
})npx @lifeprompt/acli repl ./tools.ts
acli v0.7.3 — Interactive REPL
Loaded 2 command(s) from ./tools.ts
Type 'help' for commands, '.exit' to quit
acli> add 10 20
{ "result": 30 }
acli> greet Alice --shout
{ "message": "HELLO, ALICE!" }
acli> help
{ "commands": [{ "name": "add", ... }, { "name": "greet", ... }] }
acli> exit
Bye!Single command execution (useful for scripting):
npx @lifeprompt/acli exec ./tools.ts "add 1 2"
# → { "result": 3 }TypeScript support: Works natively on Node.js 22.6+, Bun, and Deno. For older Node.js, install jiti:
npm install -D jiti
Argument Definition
ACLI uses Zod for type-safe argument parsing with rich validation.
arg(schema, meta?)
Wraps a Zod schema with CLI metadata:
import { z } from "zod"
import { arg } from "@lifeprompt/acli"
// Basic types
arg(z.string()) // Required string
arg(z.coerce.number()) // Number (coerced from string)
arg(z.coerce.number().int()) // Integer
arg(z.boolean().default(false)) // Flag (presence = true)
arg(z.array(z.string())) // Array (--tag a --tag b → ["a", "b"])
arg(z.coerce.date()) // Date (ISO8601 string → Date)
// Validation
arg(z.string().min(1).max(100)) // Length validation
arg(z.coerce.number().min(0).max(100)) // Range validation
arg(z.enum(["json", "csv", "table"])) // Enum validation
arg(z.string().email()) // Email validation
arg(z.string().regex(/^[a-z]+$/)) // Regex validation
// Optional & defaults
arg(z.string().optional()) // Optional
arg(z.string().default("hello")) // With default
// Metadata
arg(z.string(), { positional: 0 }) // Positional argument
arg(z.string(), { short: 'n' }) // Short alias (-n)
arg(z.string(), { description: "Name" }) // Help text
arg(z.string(), { examples: ["foo"] }) // Example valuesInferArgs<T>
Infers the parsed argument types from an args definition:
const myArgs = {
name: arg(z.string()),
count: arg(z.coerce.number().default(10)),
active: arg(z.boolean().optional()),
}
type MyArgs = InferArgs<typeof myArgs>
// { name: string; count: number; active?: boolean }Command Definition
Structure
import { z } from "zod"
import { defineCommand, arg, type InferArgs } from "@lifeprompt/acli"
interface CommandDefinition<TArgs extends ArgsDefinition> {
description: string // Required
args?: TArgs // Zod-based arguments
handler?: (args: InferArgs<TArgs>) => Promise<unknown>
subcommands?: CommandRegistry // Nested commands
}Example with Subcommands
Use cmd() (alias for defineCommand) inside subcommands to enable type inference:
import { z } from "zod"
import { defineCommand, cmd, arg } from "@lifeprompt/acli"
const calendar = defineCommand({
description: "Calendar management",
subcommands: {
events: cmd({
description: "Manage events",
subcommands: {
list: cmd({
description: "List events",
args: {
from: arg(z.coerce.date().optional()),
limit: arg(z.coerce.number().int().default(10)),
},
handler: async ({ from, limit }) => {
// from: Date | undefined, limit: number (types inferred!)
return { events: await fetchEvents({ from, limit }) }
},
}),
create: cmd({
description: "Create event",
args: {
title: arg(z.string().min(1)),
date: arg(z.coerce.date()),
},
handler: async ({ title, date }) => {
// title: string, date: Date (types inferred!)
return { event: await createEvent({ title, date }) }
},
}),
},
}),
},
})
// Use directly: registerAcli(server, "cli", { calendar })Note: Without
cmd(), inline subcommand handlers receiveunknowntypes due to TypeScript's type inference limitations. Always wrap subcommands withcmd()for full type safety.
Usage:
calendar events list --from 2026-02-01 --limit 5
calendar events create --title "Meeting" --date 2026-02-02T10:00:00ZPositional Arguments
Positional arguments allow cleaner syntax:
const add = defineCommand({
description: "Add numbers",
args: {
a: arg(z.coerce.number(), { positional: 0 }),
b: arg(z.coerce.number(), { positional: 1 }),
},
handler: async ({ a, b }) => ({ result: a + b }),
})
// Use: registerAcli(server, "math", { add })All syntaxes work:
add 10 20 # Positional
add --a 10 --b 20 # NamedTo use short options like -a, define them explicitly with the short metadata:
const add = defineCommand({
description: "Add numbers",
args: {
a: arg(z.coerce.number(), { positional: 0, short: 'a' }),
b: arg(z.coerce.number(), { positional: 1, short: 'b' }),
},
handler: async ({ a, b }) => ({ result: a + b }),
})
// Now supports: add -a 10 -b 20Flag Negation (--no- prefix)
Boolean flags can be explicitly set to false using the --no- prefix:
command --no-verbose # verbose = false
command --no-color # color = falseRepeated Options (Arrays)
Arguments defined with z.array(...) accumulate values from repeated options:
const search = defineCommand({
description: "Search files",
args: {
ext: arg(z.array(z.string()), { short: 'e', description: "File extensions" }),
},
handler: async ({ ext }) => ({ extensions: ext }),
})
// search --ext .ts --ext .tsx → ext: [".ts", ".tsx"]
// search -e .ts -e .tsx → ext: [".ts", ".tsx"]Built-in Commands
These commands are automatically available:
| Command | Description |
|------------|--------------------------------------|
| help | List all commands |
| help <cmd> | Show command details |
| schema | JSON schema for all commands |
| schema <cmd> | JSON schema for specific command |
| version | Show ACLI version |
Response Format
ACLI uses MCP-native response format for seamless integration.
Handler Return Values
Handlers can return values in two ways:
// 1. Simple object (auto-wrapped to MCP format)
handler: async () => ({ result: 123 })
// → { content: [{ type: "text", text: '{"result":123}' }] }
// 2. MCP native format (passed through as-is)
handler: async () => ({
content: [
{ type: "text", text: "Hello" },
{ type: "image", data: "base64...", mimeType: "image/png" },
]
})
// → passed through unchangedError Codes
| Code | Description |
|--------------------|------------------------------------------|
| COMMAND_NOT_FOUND| Command does not exist |
| VALIDATION_ERROR | Invalid arguments or missing required |
| EXECUTION_ERROR | Handler threw an error |
| PARSE_ERROR | Malformed command string |
| PERMISSION_DENIED| Authorization failed |
Security
ACLI is designed with security in mind:
- No Shell Execution: Commands are parsed and executed directly in-process
- Command Whitelist: Only registered commands can be executed
- Argument Validation: Zod validation before handler execution
- DoS Prevention: Length and count limits on commands and arguments
API Reference
registerAcli(server, name, commands, description?)
Register commands as an MCP tool.
registerAcli(server, "tool_name", commands)
// With description
registerAcli(server, "tool_name", commands, "Base description.")
// → "Base description. Commands: cmd1, cmd2. Run 'help' for details."runCli({ commands, args? })
Run as standalone CLI.
runCli({ commands }) // Uses process.argv
runCli({ commands, args: ["add", "1", "2"] }) // Custom argscreateAcli(commands)
Create a tool definition for manual integration.
const tool = createAcli(commands)
const result = await tool.execute({ command: "add 1 2" })CLI (npx @lifeprompt/acli)
npx @lifeprompt/acli repl <file> # Interactive REPL
npx @lifeprompt/acli exec <file> <command> # Single command execution
npx @lifeprompt/acli --help # Show help
npx @lifeprompt/acli --version # Show versionThe <file> should export ACLI commands via default export, named commands export, or individual named exports.
TypeScript Types
All types are exported:
import type {
// Argument types
ArgSchema,
ArgMeta,
ArgsDefinition,
InferArgs,
// Command types
CommandDefinition,
CommandRegistry,
// MCP migration types
McpToolLike,
// MCP response types
CallToolResult,
TextContent,
ImageContent,
// Error types
AcliError,
AcliErrorCode,
// Options
AcliToolOptions,
CliOptions,
} from "@lifeprompt/acli"
// Helper functions
import { arg, defineCommand, cmd, aclify } from "@lifeprompt/acli"
// cmd is an alias for defineCommand - use inside subcommands for type inference
// aclify converts MCP-style tool definitions to ACLI CommandRegistryDocumentation
- Examples - Step-by-step examples
- Migration Guide - Migrate from MCP SDK to ACLI
- AGENTS.md - Guide for coding agents (Cursor, Copilot, etc.)
- Architecture - Internal design and module structure
- Specification - Full protocol specification
Contributing
See CONTRIBUTING.md for development setup, release flow, and guidelines.
License
MIT
