# Scripting routing via MCP

Every click in the matrix routes through an action. Every action is also MCP-callable. So the assistant (or a headless MCP client like Claude Desktop) can read the routing table, build wirings, swap them in and out, and tear them down — same engine, no hidden side door.

```yaml
actions:
  - action_id: patchbay.toggle
    input: { open: true }
    label: "▶ 0. Open the Patchbay (watch the matrix)"
  - action_id: routing.list
    input: {}
    save_as: rt
    label: "▶ 1. Snapshot the current routing table"
  - action_id: documents.add
    input: { type: builder, title: "Patchbay demo" }
    save_as: b
    label: "▶ 2. Spawn a demo Builder"
  - action_id: builder.graph.node.add
    input:
      module_id: "{{b.module_id}}"
      kind: midi_note
    save_as: mn
    label: "▶ 3. Add a midi_note node"
  - action_id: builder.graph.node.add
    input:
      module_id: "{{b.module_id}}"
      kind: osc
    save_as: osc
    label: "▶ 4. Add an osc"
  - action_id: builder.graph.node.add
    input:
      module_id: "{{b.module_id}}"
      kind: output
    save_as: out
    label: "▶ 5. Add an output"
  - action_id: builder.graph.connect
    input:
      module_id: "{{b.module_id}}"
      source_node_id: "{{mn}}"
      source_port: freq
      target_node_id: "{{osc}}"
      target_port: freq_in
    label: "▶ 6. Wire midi_note.freq → osc"
  - action_id: builder.graph.connect
    input:
      module_id: "{{b.module_id}}"
      source_node_id: "{{osc}}"
      target_node_id: "{{out}}"
      target_port: in_l
    label: "▶ 7. Wire osc → output"
  - action_id: dsp.run
    input: { module_id: "{{b.module_id}}" }
    label: "▶ 8. Run it — THIS registers its MIDI sink in the matrix"
  - action_id: routing.connect
    input:
      from_source: keyboard-default
      from_port: out
      to_sink: "{{b.module_id}}:midi-in"
      to_port: in
      kind: midi
    save_as: kbd_wire
    label: "▶ 9. Wire Keyboard → the demo Builder's MIDI in"
  - action_id: routing.set
    input:
      connection_id: "{{kbd_wire.id}}"
      enabled: false
    label: "▶ 10. Suspend the wire (notes won't pass)"
  - action_id: routing.set
    input:
      connection_id: "{{kbd_wire.id}}"
      enabled: true
    label: "▶ 11. Resume the wire"
  - action_id: routing.disconnect
    input:
      connection_id: "{{kbd_wire.id}}"
    label: "▶ 12. Drop the wire entirely"
  - action_id: dsp.stop
    input: { module_id: "{{b.module_id}}" }
    label: "■ 13. Stop the demo DSP"
```

> 🟦 **HANDS-FREE PATH**: the chain shows the full lifecycle — read, connect, suspend, resume, disconnect — and it's self-contained: a fresh project has no bundled instrument, so steps 2–7 build a throwaway MIDI-responsive Builder. The load-bearing detail is step 8: **a Builder's `<module_id>:midi-in` sink only appears in the routing table while the DSP RUNS** — the worklet's MIDI subscription is the binding, not the node on the canvas. (That's also why a silent Patchbay row for your own DSP usually means: not running.) Step 9 wires the Keyboard to it and saves the connection as `kbd_wire`; steps 10–11 suspend + resume it by id; 12–13 tear down. To wire YOUR OWN DSP instead, read its id from step 1's `routing.list` and use `<module-id>:midi-in`. Close the demo Builder tab afterwards — it was never saved. Hit the **Reset** button (top-right of the actions block) to clear the saved chain state before re-running.

## The four actions

All under the `ROUTING` category in `palette.list`.

### `routing.list`

Returns the snapshot: `{ sources, sinks, connections }`. Lightweight (no audio buffers, no event history), safe to call as often as you want. Identical payload to `system.state { sections: ["routing"] }` — use `routing.list` for routing-only sweeps so the Activity log reads cleanly.

The full table grows with the project. Two optional filters narrow it: `kind` (`"audio"`, `"midi"` or `"modulation"`) keeps only that kind in all three sections; `module_id` keeps that module's own sources and sinks plus every connection touching them.

```json
{ "kind": "audio", "module_id": "builder-mp91" }
```

### `routing.connect`

Add (or replace) a connection.

```json
{
  "from_source": "<source id>",
  "from_port": "<port id on the Source>",
  "to_sink": "<sink id>",
  "to_port": "<port id on the Sink>",
  "kind": "audio" | "midi" | "modulation",
  "gain": 1.0,       // audio only, default 1
  "amount": 1.0,     // modulation only, default 1
  "enabled": true    // default true
}
```

Returns the created connection with its auto-generated id. Idempotent on the deterministic id `conn:<from_source>:<from_port>-><to_sink>:<to_port>` — re-calling with the same endpoints **replaces** rather than duplicates.

### `routing.disconnect`

```json
{ "connection_id": "<id from routing.list or routing.connect>" }
```

Idempotent — disconnecting an unknown id is a silent no-op.

### `routing.set`

Update `gain` and/or `enabled` on an existing connection without dropping + recreating it.

```json
{
  "connection_id": "<id>",
  "gain": 0.5,       // audio cables only; MIDI rejects this field
  "enabled": false
}
```

5 ms ramp applies to audio gain + enable/disable. Use `enabled: false` for an A/B mute that preserves the wire (the matrix's click toggle is connect/disconnect; `routing.set { enabled }` is the surgical option for keeping the wire in place).

## Discovering ids before connecting

The JACK-style addresses (`<id>:<port>`) are stable per entity but you usually need to discover them. Two patterns:

### From routing.list

```yaml
# 1. routing.list → inspect `sources[]` and `sinks[]` for the ones you want
# 2. read each entry's `id` and its first `outputs[0].id` / `inputs[0].id`
# 3. plug into routing.connect
```

For permanent entities the ids are stable across sessions:

- `keyboard-default`, `sequencer-default` (MIDI Sources, port `out`)
- `master` (audio Sink, port `in`; also an audio Source, port `out`)
- `recorder-default` (audio Sink, port `in`)
- `midi-input-<deviceId>` / `midi-output-<deviceId>` (hardware, one per device).

There is no bundled instrument. User-created entities — Builders + Faust DSPs (`builder-<short hash>`, generated), MixerTracks (`mtrack-<…>`, as both an audio Sink `:in` and an audio Source), and an installed instrument's `:midi-in` / modulation ports — have generated ids; pull the current ones from `routing.list`.

### From documents.list

`documents.list` lists the currently-open modules and their `module_id`s. The routing-table id for a Builder is the same as its `module_id`. Useful when you opened the Builder yourself and want to wire it without a `routing.list` round-trip.

## A common end-to-end task: wire your Builder to master

1. `documents.add { type: "builder" }` → you have a `module_id`.
2. `builder.graph.node.add { module_id, kind: "osc" }` and `builder.graph.node.add { module_id, kind: "output" }` to build the DSP.
3. `builder.graph.connect { module_id, source_node_id, target_node_id }` for the wire.
4. `dsp.run { module_id }` to compile + start.
5. The auto-routing seed has already wired `<module_id> → master` for you. If you want to verify: `routing.list` and look for the connection with `from.sourceId === module_id`.
6. If you want to tap the same DSP into the Recorder ALSO: `routing.connect { from_source: module_id, from_port: "out", to_sink: "recorder-default", to_port: "in", kind: "audio" }`. The Builder now feeds both master and Recorder.

## Watching every edit

`activity.recent` returns the rolling log of every action the registry has dispatched, including the ones the matrix clicks fire. When you're scripting a routing change, you can verify it landed by inspecting Activity's tail (your call lands as one entry; the engine's downstream reconciliation does NOT add separate rows because it's not an action).

The Activity rail panel on the right shows the same stream live, filterable by `mcp` vs `palette` (UI clicks) so you can tell which side fired what.

## Where to go from here

- **FaustWave — Mixer & Master** for the per-strip fader ↔ audio-cable-gain mapping.
- **FaustWave — AI Assistant** for the discipline behind "hot-path" tools (`dsp.*`, `routing.*` are dedicated) vs `palette.run`-only actions, and how to drive routing from a chat conversation.
- **FaustWave — Reference** for the full MCP action catalog with input schemas.
