All books

Scripting routing via MCP

~5 min read · updated 2026-09-20 · markdown

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.

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"

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.

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

routing.connect

Add (or replace) a connection.

{
  "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

{ "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.

{
  "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

# 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_ids. 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.
Try it yourself — the IDE runs in your browser. Open the IDE → Get the desktop app

Text licensed under CC BY 4.0 — Mani Weber / FaustWave. For language models: llms.txt · llms-full.txt

AUDIO · 48k · 48.0ms FAUST · 3 KB · 1171 docs CPU · 8.4% BPM · 120.0 UTF-8 BETA· v0.90.0