ollie/doc/tool-definitions.md

6.5 KiB

Tool Definitions

A .meta file can define a tool without any companion executable. The cmd field specifies a shell command to run — the registry executes it via shell, passing the tool's JSON arguments on stdin.

This is the same .meta schema used in writing-tools.md, but without the sidecar executable. No script, no binary — just the definition.

Resolution

When the registry resolves a tool, it checks:

  1. ~/.config/ollie/tools/<name> — local script or binary
  2. .meta cmd field — shell command, executed directly
  3. exec.LookPath(<name>) — PATH lookup

If a .meta file has a cmd field and no co-located executable exists, the registry runs the command via shell.

Structure

{
  "description": "One-liner for listings.",
  "prompt": "## my_tool\n\nUsage docs.\n\n```\nmy_tool(arg=\"value\")\n```",
  "cmd": "jq -r '.arg' | xargs my-cli",
  "args": {
    "type": "object",
    "required": ["arg"],
    "properties": {
      "arg": {"type": "string", "description": "Some argument"}
    }
  },
  "scope": "global"
}

The cmd field is the only addition to the standard .meta schema. All other fields work the same as documented in writing-tools.md.

Parsing arguments

The model sends JSON on stdin. Parse with jq:

{
  "cmd": "input=$(cat); arg=$(echo \"$input\" | jq -r '.arg'); my-cli \"$arg\""
}

For subcommand-style CLIs with variable arguments:

{
  "cmd": "input=$(cat); cmd=$(echo \"$input\" | jq -r '.command'); args=$(echo \"$input\" | jq -r 'del(.command) | to_entries | map(\"--\\(.key)=\\(.value|tostring)\") | join(\" \")'); eval \"my-cli $cmd $args\"",
  "args": {
    "type": "object",
    "required": ["command"],
    "properties": {
      "command": {"type": "string", "description": "Subcommand"}
    },
    "additionalProperties": true
  }
}

This extracts a command field, builds --key=value flags from remaining fields, and calls my-cli <command> --key=value ....

Example: ripgrep

{
  "description": "Search file contents with ripgrep.",
  "prompt": "## rg_search\n\nSearch files.\n\n**Args**: `pattern` (required), `path`, `glob`, `case_insensitive`\n\n```\nrg_search(pattern=\"TODO\", path=\"src/\", glob=\"*.go\")\n```",
  "cmd": "input=$(cat); p=$(echo \"$input\" | jq -r '.pattern'); dir=$(echo \"$input\" | jq -r '.path // \".\"'); glob=$(echo \"$input\" | jq -r '.glob // empty'); ci=$(echo \"$input\" | jq -r '.case_insensitive // empty'); rg ${ci:+-i} ${glob:+-g \"$glob\"} \"$p\" \"$dir\"",
  "args": {
    "type": "object",
    "required": ["pattern"],
    "properties": {
      "pattern": {"type": "string", "description": "Regex pattern"},
      "path": {"type": "string", "description": "Search directory"},
      "glob": {"type": "string", "description": "File glob filter"},
      "case_insensitive": {"type": "boolean", "description": "Case-insensitive search"}
    }
  },
  "scope": "read"
}

Example: kubectl

{
  "description": "Kubernetes cluster operations.",
  "prompt": "## k8s\n\nKubernetes operations.\n\n**Args**: `command` (required), plus command-specific args.\n\n| Command | Args |\n|---------|------|\n| `get` | `resource`, `name?`, `namespace?`, `output?` |\n| `describe` | `resource`, `name`, `namespace?` |\n| `logs` | `pod`, `namespace?`, `tail?`, `follow?` |\n| `apply` | `file` |\n| `delete` | `resource`, `name`, `namespace?` |\n\n```\nk8s(command=\"get\", resource=\"pods\", namespace=\"default\")\nk8s(command=\"logs\", pod=\"api-7d4b8c6f9-x2k4m\", tail=100)\nk8s(command=\"apply\", file=\"deploy.yaml\")\n```",
  "cmd": "input=$(cat); cmd=$(echo \"$input\" | jq -r '.command'); case $cmd in get) res=$(echo \"$input\" | jq -r '.resource'); name=$(echo \"$input\" | jq -r '.name // empty'); ns=$(echo \"$input\" | jq -r '.namespace // empty'); out=$(echo \"$input\" | jq -r '.output // empty'); kubectl get $res $name ${ns:+-n $ns} ${out:+-o $out} ;; describe) res=$(echo \"$input\" | jq -r '.resource'); name=$(echo \"$input\" | jq -r '.name'); ns=$(echo \"$input\" | jq -r '.namespace // empty'); kubectl describe $res $name ${ns:+-n $ns} ;; logs) pod=$(echo \"$input\" | jq -r '.pod'); ns=$(echo \"$input\" | jq -r '.namespace // empty'); tail=$(echo \"$input\" | jq -r '.tail // empty'); follow=$(echo \"$input\" | jq -r '.follow // empty'); kubectl logs $pod ${ns:+-n $ns} ${tail:+--tail=$tail} ${follow:+-f} ;; apply) file=$(echo \"$input\" | jq -r '.file'); kubectl apply -f \"$file\" ;; delete) res=$(echo \"$input\" | jq -r '.resource'); name=$(echo \"$input\" | jq -r '.name'); ns=$(echo \"$input\" | jq -r '.namespace // empty'); kubectl delete $res $name ${ns:+-n $ns} ;; *) echo \"unknown command: $cmd\" >&2; exit 1 ;; esac",
  "args": {
    "type": "object",
    "required": ["command"],
    "properties": {
      "command": {"type": "string", "description": "Operation (get, describe, logs, apply, delete)"}
    },
    "additionalProperties": true
  },
  "scope": "global"
}

Example: MCP bridge

MCP servers expose tools over JSON-RPC. CLI bridges like mcp-client-cli let you call them from the command line:

{
  "description": "GitHub operations via MCP.",
  "prompt": "## github\n\nGitHub MCP tools.\n\n**Args**: `tool` (required), plus tool-specific args.\n\n| Tool | Args |\n|------|------|\n| `search_repositories` | `query` |\n| `get_file_contents` | `owner`, `repo`, `path`, `branch?` |\n| `create_issue` | `owner`, `repo`, `title`, `body?` |\n| `list_issues` | `owner`, `repo`, `state?` |\n\n```\ngithub(tool=\"search_repositories\", query=\"language:go stars:>1000\")\ngithub(tool=\"get_file_contents\", owner=\"golang\", repo=\"go\", path=\"README.md\")\n```",
  "cmd": "input=$(cat); tool=$(echo \"$input\" | jq -r '.tool'); args=$(echo \"$input\" | jq -c 'del(.tool)'); echo \"$args\" | mcp-client-cli --server=github call \"$tool\"",
  "args": {
    "type": "object",
    "required": ["tool"],
    "properties": {
      "tool": {"type": "string", "description": "MCP tool name"}
    },
    "additionalProperties": true
  },
  "scope": "global"
}

This works for any MCP server with a CLI bridge. See doc/resources/no-mcp.md for why ollie uses this pattern instead of implementing MCP natively.

Installation

Copy the .meta file to ~/.config/ollie/tools/. The tool is live immediately — no restart needed.

cp my_tool.meta ~/.config/ollie/tools/

Notes

  • additionalProperties: true allows pass-through arguments the schema doesn't enumerate.
  • Prefix tool names to avoid collisions (acme_deploy, not deploy).
  • Test cmd pipelines: echo '{"pattern":"TODO"}' | bash -c 'YOUR_CMD'
  • Agent configs can auto-load tools via autoLoad.