mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-04 01:05:21 +08:00
Compare commits
1 Commits
bb/agent-t
...
bb/ableton
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9335a24f49 |
144
optional-skills/creative/ableton/SKILL.md
Normal file
144
optional-skills/creative/ableton/SKILL.md
Normal 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.
|
||||
103
optional-skills/creative/ableton/references/research.md
Normal file
103
optional-skills/creative/ableton/references/research.md
Normal 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`.
|
||||
95
optional-skills/creative/ableton/scripts/ableton_doctor.py
Normal file
95
optional-skills/creative/ableton/scripts/ableton_doctor.py
Normal 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())
|
||||
72
tests/skills/test_ableton_optional_skill.py
Normal file
72
tests/skills/test_ableton_optional_skill.py
Normal 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
|
||||
Reference in New Issue
Block a user