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.
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, portout)master(audio Sink, portin; also an audio Source, portout)recorder-default(audio Sink, portin)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
documents.add { type: "builder" }→ you have amodule_id.builder.graph.node.add { module_id, kind: "osc" }andbuilder.graph.node.add { module_id, kind: "output" }to build the DSP.builder.graph.connect { module_id, source_node_id, target_node_id }for the wire.dsp.run { module_id }to compile + start.- The auto-routing seed has already wired
<module_id> → masterfor you. If you want to verify:routing.listand look for the connection withfrom.sourceId === module_id. - 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) vspalette.run-only actions, and how to drive routing from a chat conversation. - FaustWave — Reference for the full MCP action catalog with input schemas.