# Scripting the Sequencer via MCP

The full Sequencer surface is MCP-callable — every UI gesture has an action behind it, plus a few that only show up here (`sequencer.state`, `sequencer.notes.transpose`, the scene actions). Together they're ≈30 actions covering grid, patterns, scenes, transport, undo, panel layout, and progression insertion.

This section is the catalog + a playable "build a 2-track loop from scratch" chain.

## Multi-instance: `instance_id`

Every action takes an optional `instance_id`. Omit to target the **active** sequencer (the one with UI focus / the one a fresh project lands on, `sequencer-default`). Pass a specific id to address it directly. Discover instances via `routing.list` — each sequencer is a Source with `kind: "midi"` and a stable `sequencer-<id>` shape.

Multiple instances play independently and concurrently (drum-seq + harmony-seq + bass-seq is a normal setup).

## The full catalog by area

### Transport + state

| Action | Purpose |
|---|---|
| `sequencer.play` | Start the sequencer against the master clock. Per-instance. Returns once a single pass completes in `single` loop mode, or after the first pass with a "looping in background" message in `infinite`. |
| `sequencer.stop` | Cancel at the next step boundary. "No sequence playing" when nothing is in flight. |
| `sequencer.state { instance_id? }` | Snapshot of everything: `{ instance_id, steps_per_pattern, steps_ms, loop_mode, playing, key, active_track_id, tracks, scenes, active_scene_id }`. Steps are sparse — only occupied steps are listed, each with its index `i`. |
| `sequencer.loop.set { loop: true \| false }` | Set / clear loop mode (mirrors the panel's Repeat toggle). |
| `surface.open { surface: "faustwave-bundled/sequencer#roll" \| "…#patterns" }` | Show the roll or the launching surface (no audio impact). | UI layout switch (no audio impact). |

### Tracks

| Action | Purpose |
|---|---|
| `sequencer.track.add { name? }` | Append. Returns `{ ok, track_id }`. |
| `sequencer.track.remove { track_id }` | Delete. Refuses the last track. |
| `sequencer.track.rename { track_id, name }` | Rename. |
| `sequencer.track.enabled.set { track_id, enabled }` | Mute (cursor still advances). |
| `sequencer.track.soloed.set { track_id, soloed }` | Solo wins over un-muted. |
| `sequencer.track.midi-channel.set { track_id, channel }` | 0 = Omni, 1–16 = specific. |
| `sequencer.active-track.set { track_id }` | Change which track has UI chrome focus. |

### Steps

| Action | Purpose |
|---|---|
| `sequencer.step.set { step, note? \| chord? \| rest? \| clear?, velocity?, track_id? }` | Set or clear a cell. Pass exactly one of note / chord / rest / clear. Velocity default 100. |
| `sequencer.step.duplicate { step, track_id? }` | Copy previous step (index-1) onto step. Errors on step 0 or rest source. |
| `sequencer.step.smear { from, to, track_id? }` | Copy `from` into every REST cell in `[from..to]`. Never overwrites. |
| `sequencer.step.gate.set { step, hold, track_id? }` | Gate length on an existing note/chord step. |
| `sequencer.step.length.set { step, ms }` | Pattern-level step duration override; `ms: null` reverts. |
| `sequencer.step.length.clear-all { instance_id? }` | Reset every step's length override. |
| `sequencer.notes.transpose { semitones, steps?, track_id? }` | Pitch shift. Clamped to C0..C8 as a whole. |
| `sequencer.clear { track_id? }` | Reset every step on the track to rest. |
| `sequencer.pattern.length.set { length: 8 \| 16 \| 32 \| 64 }` | Change pattern length live. |
| `sequencer.key.set { tonic_name? \| tonic_pc?, scale_id }` | Set the musical key (drives grid highlight + progression transposition). |

### Patterns

| Action | Purpose |
|---|---|
| `sequencer.pattern.add { track_id? }` | Append a pattern to a track. Up to 8. Returns `{ ok, pattern_id }`. |
| `sequencer.pattern.remove { pattern_id, track_id? }` | Delete. Refuses the last pattern. |
| `sequencer.pattern.rename { pattern_id, label, track_id? }` | Rename. |
| `sequencer.pattern.active.set { pattern_id, track_id? }` | Launch — queues at quantize boundary when playing, immediate when stopped. |
| `sequencer.pattern.edit.set { pattern_id, track_id? }` | View-only — switches which pattern the piano-roll shows. |
| `sequencer.pattern.quantize.set { pattern_id, quantize, track_id? }` | Per-pattern launch quantize. Five values: `instant`, `next-beat`, `next-bar` (default), `next-2-bars`, `next-4-bars`. |

### Scenes

| Action | Purpose |
|---|---|
| `sequencer.scene.add { from_current?, label? }` | Snapshot current activePatternId per track as a scene. Returns `{ ok, scene_id }`. |
| `sequencer.scene.remove { scene_id }` | Delete. |
| `sequencer.scene.rename { scene_id, label }` | Rename. |
| `sequencer.scene.launch { scene_id }` | Queue each track's assigned pattern via per-pattern quantize. |

### Progressions

| Action | Purpose |
|---|---|
| `sequencer.progressions.list` | Enumerate the 8 documented progressions: axis-major, axis-minor, i-iv-v-major, ii-v-i, i-vi-iv-v, minor-pop-loop, folk-minor, twelve-bar-blues. |
| `sequencer.progression.insert { progression_id, distribution, key?, track_id? }` | Drop onto the track. `distribution: "bar"` (default, 4 chords/16 steps) or `"half-bar"` (8 chords). `key` optionally transposes; omit to use the progression's canonical key. |

### Undo

| Action | Purpose |
|---|---|
| `sequencer.undo` | Pop one edit. "No history" when empty. |
| `sequencer.redo` | Repush. Clears whenever a fresh edit is made. |

## A build-from-scratch chain

```yaml
actions:
  - action_id: sequencer.track.add
    input: { name: "Chords" }
    save_as: chords
    label: "▶ 1. Add a Chords track (self-contained — your tracks stay untouched)"
  - action_id: sequencer.key.set
    input: { tonic_name: A, scale_id: aeolian }
    label: "▶ 2. Key = A minor"
  - action_id: sequencer.progression.insert
    input:
      progression_id: axis-minor
      distribution: bar
      track_id: "{{chords.track_id}}"
    label: "▶ 3. Drop axis-minor chords on bars"
  - action_id: sequencer.track.add
    input: { name: Bass }
    save_as: bass
    label: "▶ 4. Add a Bass track"
  - action_id: sequencer.active-track.set
    input:
      track_id: "{{bass.track_id}}"
    label: "▶ 5. Focus Bass"
  - action_id: sequencer.step.set
    input:
      track_id: "{{bass.track_id}}"
      index: 0
      note: 33
      velocity: 110
    label: "▶ 6. Root note on step 0 (A1, accent)"
  - action_id: sequencer.step.smear
    input:
      track_id: "{{bass.track_id}}"
      from: 0
      to: 15
    label: "▶ 7. Smear the root across the bar"
  - action_id: sequencer.loop.set
    input: { loop: true }
    label: "▶ 8. Loop mode = infinite"
  - action_id: sequencer.play
    input: {}
    label: "▶ 9. Play"
  - action_id: sequencer.stop
    input: {}
    label: "■ 10. Stop"
```

> 🟦 **HANDS-FREE PATH**: clears the active track, sets the key, drops a chord progression, adds a Bass track, focuses it, paints a root note with accent velocity, smears it across the bar (because the smear is REST-only it auto-fills the empty cells), arms infinite loop, plays, stops. Run it once to hear the result, hit **Reset** between runs.

## Driving multi-instance from MCP

For a setup with two sequencers (e.g. drum sequencer + harmony sequencer), pass `instance_id` explicitly:

```json
{ "action_id": "sequencer.play", "input": { "instance_id": "sequencer-drum" } }
```

A combined `sequencer.play` for every instance is just N parallel calls; they're rate-limited by the master clock so they'll start in lockstep anyway.

## A look at `sequencer.state`

The "what's on the grid right now" call. One call returns the whole sequencer — every track, every pattern, the scene catalog:

```json
{
  "instance_id": "sequencer-default",
  "steps_per_pattern": 16,
  "steps_ms": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
  "loop_mode": "infinite",
  "playing": true,
  "key": { "tonic_pc": 9, "scale_id": "aeolian" },
  "active_track_id": "track-1",
  "tracks": [
    {
      "id": "track-1", "name": "Track 1", "midiChannel": 1, "enabled": true, "soloed": false,
      "activePatternId": "pattern-a", "editPatternId": "pattern-a", "pendingPatternId": null,
      "patterns": [
        { "id": "pattern-a", "label": "A", "steps": [
          { "i": 0, "kind": "chord", "notes": [57, 60, 64] },
          { "i": 8, "kind": "note", "note": 57, "velocity": 90 }
        ] },
        { "id": "pattern-b", "label": "B", "steps": [] }
      ]
    }
  ],
  "scenes": [],
  "active_scene_id": null
}
```

**Steps are sparse.** A grid is mostly rests, so `steps` lists only the occupied steps, each with its 0-based index `i` — the same index `sequencer.step.set` takes. Every index below `steps_per_pattern` that is not listed is a rest; an empty `steps` array is an empty pattern. `activePatternId` is the pattern that plays, `editPatternId` the one step writes land in.

Useful when you want to render the grid into prose ("track 1 plays an A minor stab on step 0 and rests for the rest of the bar") or when the assistant needs to know the current state before deciding the next move.

## Where to go from here

- **FaustWave — Patchbay** — wire the sequencer to multiple instruments per channel.
- **FaustWave — Mixer & Master** — mix each instrument the sequencer drives.
- **FaustWave — AI Assistant** — ask "fill the active track with a chord progression that fits the bass line" and watch the chain run.
