Ableton Live Agent Skills
Producer Pal ships a portable Agent Skill that lets coding agents control Ableton Live through Producer Pal's REST API, with no MCP client required.
Not the same as Producer Pal Skills
Two different things are called "skills" around here. An Agent Skill, the subject of this page, is an integration package that teaches a coding agent how to connect to Producer Pal. The Producer Pal Skills are instructions the model receives after connecting, whichever route it took.
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 | Global skills location |
|---|---|
| Claude Code | ~/.claude/skills/<name>/SKILL.md |
| Codex CLI | ~/.codex/skills/<name>/SKILL.md |
| Gemini CLI | ~/.gemini/skills/<name>/SKILL.md |
When to use this vs MCP or REST
Both agent paths give the AI the same thing. The skill's bootstrap discovers the tools and their schemas (--list-tools) and calls ppal-connect, which returns the same Producer Pal Skills and context an MCP client gets from its own ppal-connect call, so neither route is "more automatic" than the other.
Use MCP when your client speaks it and isn't a coding agent: chat apps like Claude Desktop, the Chat UI, and web clients. Tools appear natively in the client and nothing has to shell out.
Use the skill with coding agents that can run commands. Because it drives the REST API directly, it can pick its own notation, toolset, and small-model mode per request and re-read the schemas mid-session, without moving your Chat UI or any other client off theirs. MCP clients bake the tool descriptions at the start of a conversation, so the same switch means starting a new one.
For scripts and pipelines with no agent involved, skip both and use the REST API directly.
Install
Download producer-pal-skill.zip and unzip it into your agent's skills directory:
curl -L https://producer-pal.org/downloads/producer-pal-skill.zip -o /tmp/ppal-skill.zip
unzip -o /tmp/ppal-skill.zip -d ~/.claude/skills/
# or ~/.codex/skills/, or ~/.gemini/skills/The zip holds one folder, producer-pal/, so it lands in the right place with no reshuffling. It tracks the repo's main branch, same as this site.
Those paths install it globally, for every project. Most agents also read a skills folder inside a project (for Claude Code that's .claude/skills/ in the project root), which keeps the skill to that one project. Consult your coding agent's docs for its exact project-local path.
To take it from a git checkout instead:
git clone --depth 1 https://github.com/adamjmurray/producer-pal.git
cp -r producer-pal/examples/skills/producer-pal ~/.claude/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:
- List tools.
node ppal.mjs --list-tools --notation midi-jsonreturns the full tool catalog with input schemas, so the agent knows what's available without baking it into the skill. The notation is baked into every tool and argument description, so it rides on this first call already. Coding agents getmidi-json(MIDI notes as a JSON array) because they can generate and parse it programmatically. - Call
ppal-connect. Its response includes the up-to-date Producer Pal Skills (the note syntax for the notation it asked for, transforms, conventions), the same instructions an MCP client gets from its ownppal-connectcall. The skill stays small; the heavy guidance comes from Producer Pal itself. - Use the other tools per those instructions, via
node ppal.mjs <tool> [json-args] --notation midi-json.
--notation applies to the one request that carries it, so the skill passes it every time.
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.
Notation and small-model mode
Producer Pal encodes MIDI notes in one of three notations: bar|beat (the default, compact human-readable text), midi-json (notes as a JSON array), and stark (a literal type: content format with event-based drum hits). The choice changes the note syntax in every tool/argument description and in the ppal-connect Skills.
The skill asks for midi-json per request, with --notation, rather than writing the device setting, so your Chat UI and any connected MCP clients keep whatever notation they were using while the agent works in its own.
The skill also assumes small-model mode is off. Coding agents generally run capable models that handle the full tool descriptions, so it's left at the default. If you drive Producer Pal from a local coding agent on a smaller model, edit SKILL.md to pass small-model mode (which trims the tool descriptions) and consider stark instead of midi-json:
node ppal.mjs --list-tools --notation stark --small-model-modeNarrowing the toolset
--disable-tools <names> withholds tools from a request. They vanish from --list-tools, and ppal-connect comes back without the parts of the Skills that teach them, so the agent stops paying for guidance it will never use. See Choosing a Toolset.
node ppal.mjs ppal-connect --disable-tools ppal-library,ppal-create-deviceSKILL.md tells the agent to decide once and pass the same list on every call, since the header applies per request, same as --notation, and equally invisible to the Chat UI and your MCP clients. Optimizing has the numbers.
Direct Live API (advanced)
The ppal-live-api tool gives direct, low-level access to the Live Object Model for reads and writes the higher-level tools don't cover. It's off by default (absent from --list-tools). An agent can turn it on itself, though the setting is global to the device:
node ppal.mjs --set-config '{"liveApiEnabled":true}'It's not the default for a reason: the specialized tools are tuned for reliable results, while the raw Live API is easy to misuse. Reach for it for custom integrations, scripting, or debugging directly against the Live API when the standard tools aren't enough.
Saving tokens: compact responses
The REST API returns JSON by default (a parsed result plus a separate warnings list), which is what you want when the agent generates or parses MIDI data in scripts. If you're mostly reading state or having a conversational back-and-forth (not processing the data programmatically), you can save tokens by requesting the compact format instead: pass ?format=compact on tool calls (a one-line change in ppal.mjs's callTool: params.set("format", "compact")). The trade-off is that result then comes back as a raw string rather than parsed JSON, and warnings fold into it.
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:
#!/usr/bin/env node
// Producer Pal REST API client (Node 18+, no dependencies).
//
// CLI:
// node ppal.mjs --set-config '<json>'
// 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–55000)
// --set-config <json> update device settings, e.g. '{"liveApiEnabled":true}'
// --notation <name> barbeat | midi-json | stark, for this request only
// --disable-tools <names> withhold tools from this request (comma-separated)
// --small-model-mode shrink tool schemas and Skills for this request
//
// Examples:
// node ppal.mjs --list-tools --notation midi-json
// node ppal.mjs ppal-read-live-set
// node ppal.mjs ppal-read-track '{"path": "t0"}'
// node ppal.mjs ppal-create-clip '{...}' --timeout-ms 10000
// node ppal.mjs ppal-connect --disable-tools ppal-library,ppal-create-device
//
// Library:
// import { listTools, callTool, setConfig } from "./ppal.mjs";
// const { result, warnings } = await callTool("ppal-read-live-set");
const DEFAULT_BASE_URL = "http://localhost:3350";
// Three of the per-request headers. Unlike --set-config these change nothing on
// the device: each applies to the one request that carries it, so it can't move the
// chat UI or another client off its own notation or toolset. Absent ⇒ that
// client keeps the device's global setting.
const DISABLED_TOOLS_HEADER = "x-producer-pal-disabled-tools";
const NOTATION_HEADER = "x-producer-pal-notation";
const SMALL_MODEL_MODE_HEADER = "x-producer-pal-small-model-mode";
/**
* Request headers for this call's profile, omitting whichever options are
* absent. `disabledTools` is a string[] or a comma-separated string; `notation`
* is "barbeat" | "midi-json" | "stark"; `smallModelMode` is a boolean.
*
* Nothing is remembered between requests, so pass the same options on every
* call in a session — including the `listTools` call, so the schemas you read
* match the notation you'll write.
*/
function profileHeaders(options = {}) {
const names = (
Array.isArray(options.disabledTools)
? options.disabledTools.join(",")
: (options.disabledTools ?? "")
).trim();
return {
...(names ? { [DISABLED_TOOLS_HEADER]: names } : {}),
...(options.notation ? { [NOTATION_HEADER]: options.notation } : {}),
...(options.smallModelMode != null
? { [SMALL_MODEL_MODE_HEADER]: String(Boolean(options.smallModelMode)) }
: {}),
};
}
/**
* GET /api/tools — returns the full envelope `{tools: [...]}` as a parsed
* object. The tool list endpoint always returns JSON; it has no `?format`
* toggle. The profile options (see profileHeaders) shape the catalog: withheld
* tools are omitted, and the descriptions and schemas resolve against this
* request's notation and small-model mode.
*/
export async function listTools(baseUrl = DEFAULT_BASE_URL, options = {}) {
const res = await fetch(`${baseUrl}/api/tools`, {
headers: profileHeaders(options),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
}
return res.json();
}
/**
* Call a Producer Pal tool by name. The REST API defaults to `format=json`, so
* `result` is a parsed value (object/array/etc.) and warnings are surfaced as a
* separate `warnings: string[]` field.
*
* The profile options (see profileHeaders) apply to this request: a withheld
* tool 404s, `ppal-connect` returns a Skills blob matching this request's
* notation and toolset, and `notation` also decides how notes in the arguments
* are parsed and how notes in the result are formatted.
*/
export async function callTool(name, args = {}, options = {}) {
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
const params = new URLSearchParams();
if (options.timeoutMs != null) {
params.set("timeoutMs", String(options.timeoutMs));
}
const query = params.toString();
const url = query
? `${baseUrl}/api/tools/${name}?${query}`
: `${baseUrl}/api/tools/${name}`;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...profileHeaders(options),
},
body: JSON.stringify(args),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
}
return res.json();
}
/**
* POST /config — update device settings remotely and return the full updated
* config. `patch` is a partial object; the server ignores unrecognized keys and
* also silently ignores an invalid value for a known field (an unknown
* `notation` is dropped, keeping the current setting; booleans are coerced). The
* only field that rejects with a 400 is an invalid `tools` list. Because bad
* values are dropped rather than reported, read the returned config to confirm a
* setting actually took effect.
*
* Every setting here is GLOBAL to the device — it also moves the chat UI and any
* connected MCP clients. Prefer the per-request `notation`, `disabledTools`, and
* `smallModelMode` options for anything they cover. What's left that only lives
* here: `{ liveApiEnabled: true }` to turn on the advanced `ppal-live-api` tool.
*/
export async function setConfig(patch, options = {}) {
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
const res = await fetch(`${baseUrl}/config`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
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 === "--set-config") {
opts.setConfig = argv[++i];
} else if (arg === "--disable-tools") {
opts.disabledTools = argv[++i];
} else if (arg === "--notation") {
opts.notation = argv[++i];
} else if (arg === "--small-model-mode") {
opts.smallModelMode = 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 --set-config '<json>'
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–55000)
--set-config <json> update device settings, e.g. '{"liveApiEnabled":true}'
Global to the device — it moves every other client too.
--notation <name> barbeat | midi-json | stark
--disable-tools <names> withhold tools from this request (comma-separated
tool names)
--small-model-mode shrink tool schemas and Skills
--help, -h show this help
--notation, --disable-tools, and --small-model-mode apply to the ONE request
that carries them. Nothing is remembered between calls, so pass them every
time, --list-tools included.
Examples:
node ppal.mjs --list-tools --notation midi-json
node ppal.mjs ppal-read-live-set
node ppal.mjs ppal-read-track '{"path": "t0"}'
node ppal.mjs ppal-create-clip '{...}' --notation midi-json
node ppal.mjs ppal-connect --disable-tools ppal-library,ppal-create-device
`;
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, opts);
console.log(JSON.stringify(result, null, 2));
return;
}
if (opts.setConfig != null) {
let patch;
try {
patch = JSON.parse(opts.setConfig);
} catch (err) {
console.error(`Invalid JSON for --set-config: ${err.message}`);
process.exit(1);
}
const updated = await setConfig(patch, opts);
console.log(JSON.stringify(updated, 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]}`) {
try {
await 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);
}
}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. To use it, swap the node ppal.mjs commands in SKILL.md for python ppal.py and drop ppal.py into the skill folder.
Companion skills
The producer-pal skill is the connection. Two more skills build on it:
ableton-audio-generator: synthesize audio from scratch with plain Node.js DSP and place it in Live: drum kits and Drum Racks, samples for Simpler, wavetables, reverb impulse responses, and open-ended clips like drones and textures. The agent writes the DSP for what you asked for; a shared library handles WAV encoding so custom algorithms are cheap to try.ableton-analyze-audio: get audio back out of Live, in two halves that work independently. Render the mix, a single track, or one Session clip to a file: macOS only, but no API key needed, which also makes it the way to get a plain bounce or stem on disk. Analyze any audio file with Google's Gemini API for feedback on timbre, mix, and arrangement: any platform, no Ableton involved, needs aGEMINI_API_KEY. The analysis is one short script against one HTTP endpoint, so swapping in a different audio-capable model or service is a small edit.
producer-pal-all-skills.zip has all three. Unzip it the same way:
curl -L https://producer-pal.org/downloads/producer-pal-all-skills.zip -o /tmp/ppal-skills.zip
unzip -o /tmp/ppal-skills.zip -d ~/.claude/skills/Same global-vs-project choice as above: unzip into a project's own skills folder (.claude/skills/) to scope all three to that project.
One part needs macOS
ableton-analyze-audio's render step drives Live's Export dialog with AppleScript, since Live has no render API. Everything else in the bundle (audio generation and the Gemini analysis) runs anywhere Node does.
See the skills README for the full list.
Source
- Skill folder:
examples/skills/producer-pal/ - REST API reference: REST API guide