Compare commits

...

1 Commits

Author SHA1 Message Date
Brooklyn Nicholson
9335a24f49 feat(skills): add optional AbletonMCP skill
Add an optional creative/ableton skill for controlling Ableton Live through the
upstream AbletonMCP server. The skill documents the required MIDI Remote Script,
uses the canonical `uvx ableton-mcp` command, and disables upstream telemetry in
the Hermes MCP add command.

Ships a small preflight doctor and research notes; no core dependency or bundled
runtime is added.
2026-06-25 15:35:08 -05:00
4 changed files with 414 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
---
name: ableton
description: "Control Ableton Live through AbletonMCP."
version: 1.0.0
author: Hermes Agent
license: MIT
platforms: [macos, windows]
metadata:
hermes:
category: creative
tags: [Ableton, Music, DAW, MCP, MIDI, Audio, Production]
related_skills: []
prerequisites:
commands: [hermes, uvx]
---
# AbletonMCP Skill
Optional skill — **not active until installed**:
```bash
hermes skills install official/creative/ableton
```
After install, scripts live under `~/.hermes/skills/creative/ableton/`.
In prose below, `{SKILL_DIR}` means that directory (or this repo path before
install).
Connect Hermes to Ableton Live through
[ahujasid/ableton-mcp](https://github.com/ahujasid/ableton-mcp): a stdio MCP
server (`uvx ableton-mcp`) that talks to an Ableton MIDI Remote Script over a
local socket.
## When to Use
- The user wants Hermes to inspect or control an Ableton Live session.
- The user wants tracks, clips, MIDI notes, instruments/effects, transport, or
Arrangement View edits driven through MCP tools.
- The user has Ableton Live 10+ installed and can configure a MIDI Remote
Script.
## Prerequisites
- Ableton Live 10 or newer.
- `uv` / `uvx`.
- AbletonMCP Remote Script installed inside Ableton.
- Telemetry opt-out configured for the MCP server.
Run the doctor first:
```bash
python {SKILL_DIR}/scripts/ableton_doctor.py
```
## How to Run
### 1. Install the Ableton Remote Script
Download upstream `AbletonMCP_Remote_Script/__init__.py` from
<https://github.com/ahujasid/ableton-mcp> and place it at:
```text
<Ableton Remote Scripts>/AbletonMCP/__init__.py
```
Common locations:
- macOS app bundle:
`/Applications/Ableton Live*.app/Contents/App-Resources/MIDI Remote Scripts/`
- macOS user preferences:
`~/Library/Preferences/Ableton/Live XX/User Remote Scripts/`
- Windows user preferences:
`C:\Users\<User>\AppData\Roaming\Ableton\Live x.x.x\Preferences\User Remote Scripts`
- Windows install folders:
`C:\ProgramData\Ableton\Live XX\Resources\MIDI Remote Scripts\`
or `C:\Program Files\Ableton\Live XX\Resources\MIDI Remote Scripts\`
Then launch/restart Ableton Live, open Preferences → Link, Tempo & MIDI, select
`AbletonMCP` as a Control Surface, and set Input/Output to `None`.
### 2. Add the MCP server to Hermes
Use upstream's canonical stdio command (`uvx ableton-mcp`) and disable upstream
telemetry by default:
```bash
hermes mcp add ableton --command uvx --env ABLETON_MCP_DISABLE_TELEMETRY=true --args ableton-mcp
```
Only run one AbletonMCP server instance at a time (Hermes, Claude Desktop, or
Cursor — not multiple). Start a new Hermes session after adding it.
### 3. Use the tools
Ask Hermes for session/track info before making changes. Keep edits small:
- "Get information about the current Ableton session."
- "Create a 4-bar MIDI clip with a simple melody."
- "Set the tempo to 120 BPM."
- "Load a drum rack, then create a basic kick/snare pattern."
## Quick Reference
| Goal | Command / action |
| --- | --- |
| Check setup | `python {SKILL_DIR}/scripts/ableton_doctor.py` |
| Add Hermes MCP | `hermes mcp add ableton --command uvx --env ABLETON_MCP_DISABLE_TELEMETRY=true --args ableton-mcp` |
| Test tools | `hermes mcp test ableton` |
| Reconfigure tool subset | `hermes mcp configure ableton` |
| Disable telemetry | `ABLETON_MCP_DISABLE_TELEMETRY=true` |
## Procedure
1. Run `ableton_doctor.py`.
2. Install / enable the Ableton Remote Script if the socket is not reachable.
3. Add the Hermes MCP server with telemetry disabled.
4. Restart Hermes so tools load.
5. Inspect session state first (`get_session_info`, `get_track_info`) before
creating or modifying clips.
6. Save the Ableton set before broad arrangement generation.
## Pitfalls
- **Manual Ableton setup is required.** `uvx ableton-mcp` alone is not enough;
Ableton must load the Remote Script.
- **Only one MCP client instance.** Upstream warns not to run Cursor and Claude
Desktop and Hermes against the same server at once.
- **Telemetry exists upstream.** Always pass
`ABLETON_MCP_DISABLE_TELEMETRY=true` unless the user explicitly wants to
enable it.
- **It edits the live set.** Save before broad generation and keep early
requests narrow.
- **Browser paths can be large.** Ask for specific categories/paths before
loading instruments/effects.
## Verification
- `ableton_doctor.py` shows `uvx` present and socket `127.0.0.1:9877`
reachable after Ableton loads the Remote Script.
- `hermes mcp test ableton` connects and lists tools.
- A harmless read-only call (`get_session_info`) works before any mutating
operation.
See `references/research.md` for source links, tool names, and setup details.

View File

@@ -0,0 +1,103 @@
# AbletonMCP research
Primary source: <https://github.com/ahujasid/ableton-mcp>
## Shape
AbletonMCP has two parts:
1. `AbletonMCP_Remote_Script/__init__.py` — Ableton MIDI Remote Script. It
creates a local socket server inside Ableton Live.
2. `MCP_Server/server.py` — Python stdio MCP server. It connects MCP tool calls
to the Remote Script.
Upstream's canonical MCP command is:
```json
{
"mcpServers": {
"AbletonMCP": {
"command": "uvx",
"args": ["ableton-mcp"]
}
}
}
```
For Hermes:
```bash
hermes mcp add ableton --command uvx --env ABLETON_MCP_DISABLE_TELEMETRY=true --args ableton-mcp
```
`--env` must appear before `--args` because Hermes's `--args` consumes the
remaining command line.
## Remote Script setup
Upstream locations:
- macOS:
- `Contents/App-Resources/MIDI Remote Scripts/` inside the Ableton app bundle
- `~/Library/Preferences/Ableton/Live XX/User Remote Scripts`
- Windows:
- `C:\Users\<User>\AppData\Roaming\Ableton\Live x.x.x\Preferences\User Remote Scripts`
- `C:\ProgramData\Ableton\Live XX\Resources\MIDI Remote Scripts\`
- `C:\Program Files\Ableton\Live XX\Resources\MIDI Remote Scripts\`
Create `AbletonMCP/` under the Remote Scripts dir, copy `__init__.py`, restart
Ableton, then Preferences → Link, Tempo & MIDI → Control Surface:
`AbletonMCP`, Input/Output: `None`.
The Remote Script socket is observed in community docs as `127.0.0.1:9877`.
## Tool surface
Tool names from upstream `MCP_Server/server.py` include:
- `get_session_info`
- `get_track_info`
- `create_midi_track`
- `set_track_name`
- `create_clip`
- `create_audio_clip`
- `add_notes_to_clip`
- `set_clip_name`
- `set_tempo`
- `load_instrument_or_effect`
- `fire_clip`
- `stop_clip`
- `stop_playback`
- `get_browser_tree`
- `get_browser_items_at_path`
- `load_drum_kit`
- `set_arrangement_time`
- `get_arrangement_clips`
- `duplicate_to_arrangement`
Capabilities advertised by upstream: track manipulation, MIDI/audio clips,
instruments/effects, Arrangement View composition, session/transport control.
## Telemetry
Upstream collects anonymous telemetry unless disabled. Disable by default in
Hermes instructions with one of:
- `ABLETON_MCP_DISABLE_TELEMETRY=true`
- `DISABLE_TELEMETRY=true`
- `MCP_DISABLE_TELEMETRY=true`
The skill uses `ABLETON_MCP_DISABLE_TELEMETRY=true`.
## GitHub usage checks
Observed real-world usage agrees that `uvx ableton-mcp` is the canonical entry:
- `ahujasid/ableton-mcp`
- `dnkrow/claude-code-ableton-mac`
- `ParkerRex/synthia`
- `patrickking67/producer`
Some forks warn about package/version confusion, but for the upstream repo the
published PyPI package exposes console script `ableton-mcp =
MCP_Server.server:main`.

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Preflight checks for AbletonMCP.
Checks the lazy runtime bits (`uvx`) and the Ableton Remote Script socket. The
script never installs packages and never starts the MCP server.
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import socket
from pathlib import Path
ABLETON_HOST = "127.0.0.1"
ABLETON_PORT = 9877
def _port_open(host: str, port: int, timeout: float = 0.35) -> bool:
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False
def _mac_remote_script_candidates(home: Path) -> list[str]:
root = home / "Library" / "Preferences" / "Ableton"
if not root.exists():
return []
return [
str(p / "User Remote Scripts" / "AbletonMCP")
for p in sorted(root.glob("Live *"))
if p.is_dir()
]
def check(*, which=shutil.which, port_open=_port_open, home=None) -> dict:
home_path = Path(home or Path.home())
telemetry_disabled = any(
os.environ.get(key, "").lower() == "true"
for key in (
"ABLETON_MCP_DISABLE_TELEMETRY",
"DISABLE_TELEMETRY",
"MCP_DISABLE_TELEMETRY",
)
)
return {
"uvx": bool(which("uvx")),
"remote_script_socket": {
"host": ABLETON_HOST,
"port": ABLETON_PORT,
"reachable": port_open(ABLETON_HOST, ABLETON_PORT),
},
"telemetry_disabled_in_env": telemetry_disabled,
"mac_remote_script_candidates": _mac_remote_script_candidates(home_path),
}
def _summary(s: dict) -> str:
sock = s["remote_script_socket"]
lines = [
"✓ uvx found" if s["uvx"] else "✗ uvx not found — install uv first",
(
f"✓ Ableton Remote Script socket: {sock['host']}:{sock['port']} reachable"
if sock["reachable"]
else f"✗ Ableton Remote Script socket: {sock['host']}:{sock['port']} not reachable"
),
(
"✓ telemetry disabled in current env"
if s["telemetry_disabled_in_env"]
else "⚠ telemetry not disabled in current env; configure MCP env opt-out"
),
]
candidates = s["mac_remote_script_candidates"]
if candidates:
lines.append("macOS candidate Remote Script dirs:")
lines.extend(f" - {p}" for p in candidates)
return "\n".join(lines)
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description="Check AbletonMCP prerequisites.")
ap.add_argument("--json", action="store_true", help="Emit JSON")
args = ap.parse_args(argv)
status = check()
print(json.dumps(status, indent=2) if args.json else _summary(status))
return 0 if status["uvx"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,72 @@
"""Tests for the optional Ableton skill."""
from __future__ import annotations
import importlib.util
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SKILL_DIR = ROOT / "optional-skills" / "creative" / "ableton"
def _load(path: Path, name: str):
spec = importlib.util.spec_from_file_location(name, path)
assert spec and spec.loader
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _description(skill_dir: Path) -> str:
text = (skill_dir / "SKILL.md").read_text()
match = re.search(r'^description:\s*"?([^"\n]+)"?\s*$', text, re.MULTILINE)
assert match
return match.group(1)
class TestAbletonSkillShape:
def test_skill_lives_under_optional_skills(self):
assert (SKILL_DIR / "SKILL.md").is_file()
assert not (ROOT / "skills" / "creative" / "ableton").exists()
def test_description_is_catalog_sized(self):
desc = _description(SKILL_DIR)
assert len(desc) <= 60, desc
assert desc.endswith(".")
def test_optional_skill_source_fetches_skill(self):
from tools.skills_hub import OptionalSkillSource
source = OptionalSkillSource()
assert source.fetch("official/creative/ableton").name == "ableton"
class TestAbletonDoctor:
def test_uvx_socket_and_telemetry(self, monkeypatch, tmp_path):
doctor = _load(SKILL_DIR / "scripts" / "ableton_doctor.py", "ableton_doctor")
monkeypatch.setenv("ABLETON_MCP_DISABLE_TELEMETRY", "true")
live = tmp_path / "Library" / "Preferences" / "Ableton" / "Live 12"
live.mkdir(parents=True)
status = doctor.check(
which={"uvx": "/bin/uvx"}.get,
port_open=lambda host, port: (host, port) == ("127.0.0.1", 9877),
home=tmp_path,
)
assert status["uvx"] is True
assert status["remote_script_socket"]["reachable"] is True
assert status["telemetry_disabled_in_env"] is True
assert status["mac_remote_script_candidates"] == [
str(live / "User Remote Scripts" / "AbletonMCP")
]
def test_uvx_missing(self):
doctor = _load(
SKILL_DIR / "scripts" / "ableton_doctor.py", "ableton_doctor_none"
)
status = doctor.check(which=lambda _name: None, port_open=lambda *_a: False)
assert status["uvx"] is False
assert status["remote_script_socket"]["reachable"] is False