# Ableton Live Agent Skill

Producer Pal ships a portable Agent Skill that lets coding agents control
Ableton Live through Producer Pal's [REST API](/guide/rest-api) — no MCP client
required.

Agent Skills are a small, open convention shared across the major coding-agent
CLIs: a folder containing a `SKILL.md` (with frontmatter describing when to use
it) plus optional scripts and resources. The folder is loaded lazily when the
agent decides the skill is relevant. The same folder works across all three:

| Tool                                                              | Skills location                    |
| ----------------------------------------------------------------- | ---------------------------------- |
| [Claude Code](https://docs.claude.com/en/docs/claude-code/skills) | `~/.claude/skills/<name>/SKILL.md` |
| [Codex CLI](https://developers.openai.com/codex/skills/)          | `~/.codex/skills/<name>/SKILL.md`  |
| [Gemini CLI](https://geminicli.com/docs/cli/skills/)              | `~/.gemini/skills/<name>/SKILL.md` |

::: info When to use this vs MCP

Producer Pal's [MCP server](/installation) is the recommended path when your
agent supports MCP — the tools come with rich descriptions and the LLM picks
them up automatically.

The skill is for **REST-API-driven workflows**: agents not configured with the
Producer Pal MCP server, scripts and pipelines that don't run an MCP client, or
environments where you want a single drop-in folder rather than per-tool MCP
setup.

:::

## Install

Copy the
[`producer-pal/`](https://github.com/adamjmurray/producer-pal/tree/main/examples/skills/producer-pal)
folder from the Producer Pal repo into your agent's skills directory.

```bash
# Clone (or download) the repo, then copy the skill folder
git clone --depth 1 https://github.com/adamjmurray/producer-pal.git
cp -r producer-pal/examples/skills/producer-pal ~/.claude/skills/
# or ~/.codex/skills/, or ~/.gemini/skills/
```

The skill folder contains a `SKILL.md` (frontmatter + instructions for the
agent) and a `ppal.mjs` (the Node CLI it shells out to).

## How it works

When the user asks the agent something Producer-Pal-shaped ("set tempo to 120",
"what's in track 2", "make a 4-bar drum loop"), the agent loads `SKILL.md` and
follows its bootstrap:

1. **List tools** — `node ppal.mjs --list-tools` returns the full tool catalog
   with input schemas, so the agent knows what's available without baking it
   into the skill.
2. **Call `ppal-connect`** — the agent's first call. Its response includes the
   up-to-date Producer Pal Skills (bar|beat notation, MIDI syntax, code
   transforms, conventions) — the same instructions Producer Pal's MCP clients
   receive at session start. The skill stays small; the heavy guidance comes
   from Producer Pal itself.
3. **Use the other tools** per those instructions, via
   `node ppal.mjs <tool> [json-args]`.

Because the skill is just a thin pointer + bootstrap, it stays correct as
Producer Pal evolves: new tools, schema changes, and skill updates land in
`ppal-connect`'s response automatically.

## The bundled script

`ppal.mjs` is a zero-dependency Node 18+ script that wraps Producer Pal's REST
API. It's both the CLI the skill shells out to and a small library you can
`import` in your own code:

```js
#!/usr/bin/env node

// Producer Pal REST API client (Node 18+, no dependencies).
//
// CLI:
//   node ppal.mjs --list-tools
//   node ppal.mjs <tool> [json-args] [options]
//
// Options:
//   --url <baseUrl>      override Producer Pal URL (default http://localhost:3350)
//   --timeout-ms <ms>    per-request timeout (1–60000)
//
// Examples:
//   node ppal.mjs --list-tools
//   node ppal.mjs ppal-read-live-set
//   node ppal.mjs ppal-read-track '{"trackIndex": 0}'
//   node ppal.mjs ppal-create-clip '{...}' --timeout-ms 10000
//
// Library:
//   import { listTools, callTool } from "./ppal.mjs";
//   const { result, warnings } = await callTool("ppal-read-live-set");

const DEFAULT_BASE_URL = "http://localhost:3350";

/**
 * GET /api/tools — returns the full envelope `{tools: [...]}` as a parsed
 * object. The tool list endpoint always returns JSON; it has no `?format`
 * toggle.
 */
export async function listTools(baseUrl = DEFAULT_BASE_URL) {
  const res = await fetch(`${baseUrl}/api/tools`);
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return res.json();
}

/**
 * Call a Producer Pal tool by name. Always uses `?format=json` so `result` is
 * a parsed value (object/array/etc.) and warnings are surfaced as a separate
 * `warnings: string[]` field.
 */
export async function callTool(name, args = {}, options = {}) {
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
  const params = new URLSearchParams({ format: "json" });
  if (options.timeoutMs != null)
    params.set("timeoutMs", String(options.timeoutMs));

  const res = await fetch(`${baseUrl}/api/tools/${name}?${params}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(args),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return res.json();
}

// --- CLI ---

function parseArgs(argv) {
  const opts = { baseUrl: DEFAULT_BASE_URL, listTools: false };
  const positional = [];
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i];
    if (arg === "--url") opts.baseUrl = argv[++i];
    else if (arg === "--timeout-ms") opts.timeoutMs = Number(argv[++i]);
    else if (arg === "--list-tools") opts.listTools = true;
    else if (arg === "--help" || arg === "-h") opts.help = true;
    else positional.push(arg);
  }
  return { opts, positional };
}

const HELP = `Producer Pal REST API client

Usage:
  node ppal.mjs --list-tools
  node ppal.mjs <tool> [json-args] [options]

Options:
  --url <baseUrl>      override Producer Pal URL (default ${DEFAULT_BASE_URL})
  --timeout-ms <ms>    per-request timeout (1–60000)
  --help, -h           show this help

Examples:
  node ppal.mjs --list-tools
  node ppal.mjs ppal-read-live-set
  node ppal.mjs ppal-read-track '{"trackIndex": 0}'
`;

async function main(argv) {
  const { opts, positional } = parseArgs(argv);
  if (opts.help) {
    console.log(HELP);
    return;
  }

  if (opts.listTools) {
    const result = await listTools(opts.baseUrl);
    console.log(JSON.stringify(result, null, 2));
    return;
  }

  const [toolName, argsJson = "{}"] = positional;
  if (!toolName) {
    console.error(
      "Missing tool name. Use --list-tools to discover tools, or pass a tool name as the first argument.",
    );
    process.exit(1);
  }

  let args;
  try {
    args = JSON.parse(argsJson);
  } catch (err) {
    console.error(`Invalid JSON for tool args: ${err.message}`);
    process.exit(1);
  }

  const response = await callTool(toolName, args, opts);
  if (response.isError) {
    console.error(`API error: ${response.result}`);
    process.exit(1);
  }
  console.log(JSON.stringify(response, null, 2));
}

// Run main() when invoked as CLI (not when imported as a library)
if (import.meta.url === `file://${process.argv[1]}`) {
  main(process.argv.slice(2)).catch((err) => {
    if (err.cause?.code === "ECONNREFUSED") {
      console.error(
        "Could not connect to Producer Pal. Is Ableton Live running with the Producer Pal device?",
      );
    } else {
      console.error(err.message ?? err);
    }
    process.exit(1);
  });
}
```
::: tip Prefer Python?

The skill ships with the Node script because nearly every agent runtime has Node
available, but a zero-dependency Python equivalent is also maintained — see the
[Python sample script](/guide/rest-api#python). To use it, swap the
`node ppal.mjs` commands in `SKILL.md` for `python ppal.py` and drop `ppal.py`
into the skill folder.

:::

## Source

- Skill folder:
  [`examples/skills/producer-pal/`](https://github.com/adamjmurray/producer-pal/tree/main/examples/skills/producer-pal)
- REST API reference: [REST API guide](/guide/rest-api)
