# LiveMCP — Claude Code Instructions

## Project Overview

LiveMCP is a Model Context Protocol (MCP) bridge that exposes Ableton Live's internal Python API to AI assistants. Version 1.0.0, published on PyPI as `livemcp`, invoked via `uvx livemcp`.

It provides 171 tools across 7 categories: session, clips, tracks, devices, mixer, arrangement, grooves.

macOS only. The remote script relies on Ableton's macOS application bundle structure and the osascript-based install/restart scripts target macOS paths exclusively.

## Architecture

LiveMCP has two components that must stay in sync:

**MCP Server (`src/livemcp/`)** — Python 3.10+, FastMCP, communicates with Ableton over a TCP socket.

- `src/livemcp/server.py` — FastMCP instance, registers all tools
- `src/livemcp/connection.py` — singleton TCP connection to Ableton
- `src/livemcp/tools/` — one module per category, each exports a `TOOLS` list of functions

**Remote Script (`remote_script/LiveMCP/`)** — Runs inside Ableton Live's embedded Python 3.11 interpreter as a MIDI Remote Script (extends `ControlSurface`).

- `remote_script/LiveMCP/server.py` — TCP server, command dispatcher
- `remote_script/LiveMCP/handlers/` — one module per category, each exports `READ_HANDLERS` and `WRITE_HANDLERS` dicts

## Connection Details

- Host: `127.0.0.1`, Port: `9877`
- Singleton: one `AbletonConnection` instance shared across all tool calls (thread-safe via `threading.Lock`)
- Timeout: 15 seconds per command
- Retries: 3 attempts with 1-second delay between attempts
- Validation: on first connect, `get_session_info` is sent as a ping to confirm the remote script is ready

## READ vs WRITE Handler Threading Model

This is the most important rule when adding handlers in `remote_script/LiveMCP/handlers/`:

- **READ_HANDLERS** run directly on the socket thread. Safe for reading Live object properties.
- **WRITE_HANDLERS** are dispatched to Ableton's main thread via `schedule_message`. Required for any mutation of Live's object model (setting properties, calling methods that modify state).

**Putting a write operation in READ_HANDLERS will crash or corrupt Ableton's state.** When in doubt, use WRITE_HANDLERS.

## Adding a New Tool

Adding a tool requires changes in four places:

**Step 1 — Add the tool function** in `src/livemcp/tools/<category>.py`:

```python
def my_new_tool(track_index: int, value: float) -> str:
    """One-line summary for the AI assistant.

    Args:
        track_index: Zero-based track index.
        value: The value to set.
    """
    result = get_connection().send_command("my_new_tool", {
        "track_index": track_index,
        "value": value,
    })
    return json.dumps(result)
```

**Step 2 — Append to TOOLS list** at the bottom of the same file:

```python
TOOLS = [
    # ... existing tools ...
    my_new_tool,
]
```

**Step 3 — Add the handler** in `remote_script/LiveMCP/handlers/<category>.py`:

```python
def my_new_tool(control_surface, params):
    track_index = params.get("track_index")
    value = params.get("value")
    if track_index is None or value is None:
        raise ValueError("Missing required parameters")
    song = control_surface.song()
    track = song.tracks[track_index]
    # mutate state here
    return {"ok": True}
```

**Step 4 — Register in READ_HANDLERS or WRITE_HANDLERS** in the same handler file:

```python
READ_HANDLERS = {
    # read-only: runs on socket thread
    "get_something": get_something,
}

WRITE_HANDLERS = {
    # state mutation: queued to Ableton's main thread
    "my_new_tool": my_new_tool,
}
```

The command string (e.g. `"my_new_tool"`) must be identical in `send_command(...)` and the handler dict key.

## Project Structure

```
src/livemcp/
    server.py           # FastMCP instance, registers all tools from TOOLS lists
    connection.py       # AbletonConnection singleton, send_command()
    tools/
        session.py      # tempo, transport, time signature, cue points
        tracks.py       # create/delete/rename tracks, arm, solo, mute
        clips.py        # create clips, add notes, quantize, warp
        devices.py      # load instruments/effects, device parameters
        mixer.py        # volume, pan, send levels, routing
        arrangement.py  # arrangement view clips
        grooves.py      # groove pool

remote_script/LiveMCP/
    __init__.py         # ControlSurface entry point
    server.py           # TCP server + dispatcher (READ vs WRITE routing)
    handlers/
        __init__.py     # aggregates all READ/WRITE handler dicts
        session.py
        tracks.py
        clips.py
        devices.py
        mixer.py
        arrangement.py
        grooves.py

scripts/
    install.sh          # symlinks remote_script/LiveMCP into Ableton's MIDI Remote Scripts dir
    uninstall.sh        # removes the symlink from all detected Ableton versions
    restart_ableton.sh  # full restart procedure (cache clear, quit, relaunch, wait for socket)
```

## Restarting Ableton Live

When you need to restart Ableton (e.g. after modifying remote script files), **always** use this procedure:

```bash
# 1. Clear bytecode cache so Ableton loads fresh Python
find /home/alaarab/Sites/livemcp/remote_script -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null

# 2. Gracefully quit (sends save dialog — dismiss it)
osascript -e 'tell application "Ableton Live 12 Suite" to quit'

# 3. Dismiss any "Save" dialog that appears
sleep 2
osascript -e 'tell application "System Events" to tell process "Ableton Live 12 Suite" to if exists window 1 then click button "Don'\''t Save" of window 1' 2>/dev/null

# 4. Wait for process to fully exit
for i in $(seq 1 15); do
  pgrep -x "Ableton Live" > /dev/null 2>&1 || break
  sleep 1
done

# 5. Force kill if it's still hanging
pgrep -x "Ableton Live" > /dev/null 2>&1 && pkill -9 -x "Ableton Live" && sleep 2

# 6. Relaunch
open -a "Ableton Live 12 Suite"

# 7. Dismiss "unexpectedly quit" dialog if it appears (poll for a few seconds)
for i in $(seq 1 10); do
  osascript -e 'tell application "System Events" to tell process "Ableton Live 12 Suite" to if exists window 1 then click button "Reopen" of window 1' 2>/dev/null && break
  sleep 1
done

# 8. Wait for LiveMCP socket to be ready
for i in $(seq 1 30); do
  python3 -c "import socket; s=socket.socket(); s.settimeout(2); s.connect(('127.0.0.1', 9877)); s.close()" 2>/dev/null && break
  sleep 3
done
```

Or use the helper script: `bash scripts/restart_ableton.sh`

### Important Notes

- **Always clear `__pycache__`** before restarting — Ableton caches .pyc files and won't see your changes otherwise
- The "unexpectedly quit" dialog appears if the previous quit wasn't clean. The script handles this automatically.
- The "Save?" dialog appears on graceful quit. The script clicks "Don't Save" — if you need to save, do it manually first.
- Socket on port 9877 typically takes 15-30 seconds after launch to become available.

## Verification After Changes

```bash
# Syntax check all Python files
find . -name "*.py" -not -path "./.venv/*" | xargs -I{} python3 -c "import py_compile; py_compile.compile('{}', doraise=True)"

# Verify registered tool count (target: 171)
uv run python -c "from livemcp.server import mcp; print(len(mcp._tool_manager._tools), 'tools')"

# Lint with ruff (line-length 100, configured in pyproject.toml)
uv run ruff check src/livemcp/
```

## Code Style

Ruff is the linter. Line length is 100, configured in `pyproject.toml` under `[tool.ruff]`. Run `uv run ruff check src/livemcp/` before committing. Run `uv run ruff format src/livemcp/` to auto-format.

## Known API Gotchas

- `MidiNoteSpecification`: must use constructor kwargs, NOT attribute setting
- `apply_note_modifications`: pass the original tuple from `get_notes_extended`, don't convert to list
- `quantize_clip` grid: must be int, not float
- Mixer zero-value: never use `params.get("x") or params.get("y")` — use explicit None checks (0.0 is falsy)
- `scene.tempo`: can't set to -1.0 to clear; use `scene.tempo_enabled = False` instead
- `clip.groove = None` doesn't work; groove removal is an API limitation
