diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a18e48 --- /dev/null +++ b/README.md @@ -0,0 +1,661 @@ +# bitwig-mcp + +A full-featured [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for controlling **Bitwig Studio** via the [DrivenByMoss](https://github.com/git-moss/DrivenByMoss) OSC extension. Exposes **204 MCP tools** covering every OSC command DrivenByMoss supports — transport, tracks, devices, clips, scenes, browser, project, layout, MIDI, and more. + +--- + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [DrivenByMoss OSC Setup](#drivenbymoss-osc-setup) +- [Configuration](#configuration) +- [Running the Server](#running-the-server) +- [Registering with Claude Code](#registering-with-claude-code) +- [Architecture](#architecture) +- [Tool Reference](#tool-reference) + - [Transport](#transport) + - [Track](#track) + - [Master Track](#master-track) + - [Device](#device) + - [Clip](#clip) + - [Scene](#scene) + - [Browser](#browser) + - [Project](#project) + - [Layout & Panels](#layout--panels) + - [MIDI (Virtual Keyboard)](#midi-virtual-keyboard) + - [Miscellaneous](#miscellaneous) +- [Usage Examples](#usage-examples) +- [Troubleshooting](#troubleshooting) + +--- + +## Prerequisites + +- **Bitwig Studio** 4.x or later +- **DrivenByMoss** installed as a Bitwig controller extension ([download](https://github.com/git-moss/DrivenByMoss/releases)) +- **Python 3.10+** + +--- + +## Installation + +```bash +# Clone or navigate to the project +cd bitwig-mcp + +# Create a virtual environment +python -m venv .venv + +# Activate it (Windows) +.venv\Scripts\activate + +# Install the package and all dependencies +pip install -e . +``` + +**Dependencies installed:** +- `mcp[cli]` — Model Context Protocol SDK (FastMCP server) +- `python-osc` — OSC send/receive over UDP + +--- + +## DrivenByMoss OSC Setup + +1. Open **Bitwig Studio** → Preferences → **Controllers** +2. Click **Add controller** → search for **DrivenByMoss** → select **Open Sound Control (OSC)** +3. Configure the OSC extension settings: + +| DrivenByMoss Setting | Value | Meaning | +|---|---|---| +| **Port to receive on** | `8000` | DrivenByMoss listens here — bitwig-mcp sends to this port | +| **Host to send to** | `127.0.0.1` | Where DrivenByMoss sends state updates | +| **Port to send to** | `9000` | DrivenByMoss sends here — bitwig-mcp listens on this port | + +4. Enable the controller and restart Bitwig if prompted. + +> **Important:** "Port to receive on" is where DrivenByMoss accepts commands (bitwig-mcp sends TO it). "Port to send to" is where DrivenByMoss pushes state updates (bitwig-mcp listens ON it). These are from DrivenByMoss's perspective — the opposite of what you might expect. + +--- + +## Configuration + +All settings are controlled via environment variables with sensible defaults: + +| Variable | Default | Description | +|---|---|---| +| `BITWIG_HOST` | `127.0.0.1` | Host where Bitwig is running | +| `BITWIG_SEND_PORT` | `8000` | Port to send OSC commands to (DrivenByMoss receive port) | +| `BITWIG_RECEIVE_PORT` | `9000` | Port to listen for state updates on (DrivenByMoss send port) | +| `BITWIG_TIMEOUT` | `5.0` | Seconds to wait when using `wait_for_state()` | +| `BITWIG_REFRESH_WAIT` | `0.5` | Seconds to wait after sending `/refresh` for state to populate | + +Example — connecting to Bitwig on another machine: +```bash +BITWIG_HOST=192.168.1.10 BITWIG_SEND_PORT=8000 bitwig-mcp +``` + +--- + +## Running the Server + +```bash +# With venv activated +bitwig-mcp + +# Or directly via the venv executable (Windows) +.venv\Scripts\bitwig-mcp.exe + +# Or via Python +python -m bitwig_mcp.server +``` + +The server starts and listens for MCP connections on stdio (standard MCP transport). It also starts the OSC listener thread in the background immediately. + +--- + +## Registering with Claude Code + +### Global (available in all sessions) + +```bash +claude mcp add bitwig-mcp "C:\path\to\bitwig-mcp\.venv\Scripts\bitwig-mcp.exe" +``` + +### Project-scoped (`.claude/settings.json`) + +```json +{ + "mcpServers": { + "bitwig-mcp": { + "command": "C:\\path\\to\\bitwig-mcp\\.venv\\Scripts\\bitwig-mcp.exe", + "args": [] + } + } +} +``` + +### Verify it's working + +Once registered, ask Claude: +> *"Check if Bitwig is connected"* → calls `ping_bitwig` +> *"Get the current transport state"* → calls `transport_get_state` + +--- + +## Architecture + +### Push-Based OSC Protocol + +DrivenByMoss uses a **push-based** (publish-subscribe) model rather than request/response. Bitwig proactively sends OSC messages to notify about every state change. This means: + +- **No polling needed** — state arrives automatically when anything changes in Bitwig +- **State store** — `osc_client.py` maintains an in-memory dict (`address → last received args`) updated by every incoming OSC message +- **`/refresh`** — sending this special command triggers Bitwig to dump its full current state immediately (used on startup and by `refresh_state`) + +### Data Flow + +``` +Claude / LLM + │ + │ MCP tool calls + ▼ +bitwig-mcp (FastMCP server) + │ ▲ + │ cmd(addr, *args) │ OSC state updates + │ (UDP to port 8000) │ (UDP from port 9000) + ▼ │ +DrivenByMoss OSC extension (inside Bitwig Studio) + │ ▲ + │ Bitwig Java API │ + ▼ │ +Bitwig Studio ──────────┘ +``` + +### Key Components + +| File | Purpose | +|---|---| +| `src/bitwig_mcp/server.py` | FastMCP server entry point; registers all tool modules | +| `src/bitwig_mcp/config.py` | Environment variable configuration | +| `src/bitwig_mcp/osc_client.py` | Thread-safe OSC singleton: send, receive, state store | +| `src/bitwig_mcp/tools/` | One file per domain, each exports `register(mcp)` | + +### OSC Client API + +```python +from bitwig_mcp.osc_client import get_client + +c = get_client() + +# Send a command to Bitwig (fire-and-forget) +c.cmd("/play", 1) +c.cmd("/track/1/volume", 100) + +# Read cached state (returns tuple or None) +state = c.get_state("/tempo/raw") # e.g. (128.0,) +value = c.get_state_value("/play") # e.g. 1 + +# Wait for a state update (blocks up to timeout) +c.wait_for_state("/track/1/volume", timeout=2.0) + +# Trigger full state dump from Bitwig +c.refresh() + +# Get all known state as a dict +all_state = c.get_all_state() + +# Check connection +connected = c.ping() +``` + +### Value Ranges + +DrivenByMoss uses the following conventions (default resolution: 128 steps): + +| Parameter | Range | Notes | +|---|---|---| +| Volume | `0–127` | ~100 = unity gain (0 dB) | +| Pan | `0–127` | 64 = center | +| Send volume | `0–127` | ~100 = unity | +| Boolean (on/off) | `0` or `1` | Omitting triggers a toggle | +| Normalized floats | `0.0–1.0` | Device parameters, EQ settings | +| MIDI note | `0–127` | 60 = C4 (middle C) | +| MIDI velocity | `0–127` | 0 = note off | +| Track/clip indices | `1–8` | 1-indexed in OSC paths | +| Colors | `"rgb(r,g,b)"` | e.g. `"rgb(255,128,0)"` | + +--- + +## Tool Reference + +### Transport + +Tools for controlling playback, recording, tempo, loop, and automation. + +| Tool | Parameters | Description | +|---|---|---| +| `transport_play` | `value=1` | Start/stop playback. `1` = play, `0` = stop | +| `transport_stop` | — | Stop playback (rewinds if already stopped) | +| `transport_restart` | — | Restart playback from the beginning | +| `transport_toggle_record` | `value?` | Toggle/set arranger record mode | +| `transport_toggle_overdub` | `value?` | Toggle/set arranger overdub | +| `transport_toggle_launcher_overdub` | `value?` | Toggle/set clip launcher overdub | +| `transport_toggle_loop` | `value?` | Toggle/set loop (repeat) mode | +| `transport_toggle_punch_in` | `value?` | Toggle/set punch-in recording | +| `transport_toggle_punch_out` | `value?` | Toggle/set punch-out recording | +| `transport_toggle_click` | `value?` | Toggle/set metronome | +| `transport_set_click_volume` | `value` (0–100) | Set metronome volume | +| `transport_toggle_click_ticks` | `value?` | Toggle/set metronome tick marks | +| `transport_toggle_click_preroll` | `value?` | Toggle/set pre-roll click | +| `transport_quantize` | — | Quantize the cursor clip | +| `transport_set_tempo` | `bpm` (float) | Set tempo in BPM | +| `transport_tap_tempo` | — | Tap tempo (call repeatedly) | +| `transport_adjust_tempo` | `amount`, `direction` ("+"/"-") | Nudge tempo up or down | +| `transport_set_position` | `value` (float) | Set timeline position in beats | +| `transport_step_position` | `direction` ("+"/"-"), `size` ("small"/"large") | Step position forward/backward | +| `transport_goto_start` | — | Jump to beginning of arrangement | +| `transport_set_crossfade` | `value` (0–127) | Set crossfader position (64 = center) | +| `transport_reset_crossfade` | — | Reset crossfader to center | +| `transport_toggle_autowrite` | `value?` | Toggle/set arranger automation write | +| `transport_toggle_launcher_autowrite` | `value?` | Toggle/set launcher automation write | +| `transport_set_automation_mode` | `mode` ("OFF"/"LATCH"/"TOUCH") | Set automation write mode | +| `transport_set_preroll` | `measures` (int) | Set pre-roll measure count | +| `transport_set_post_recording_action` | `action` (string) | Set launcher post-recording action | +| `transport_set_post_recording_offset` | `beats` (float) | Set post-recording time offset in beats | +| `transport_set_launch_quantization` | `quantization` (string) | Set default clip launch quantization | +| `transport_get_state` | — | Get complete transport state snapshot | + +**Post-recording action values:** `"OFF"`, `"PLAY_RECORDED"`, `"RECORD_NEXT_FREE_SLOT"`, `"STOP"`, `"RETURN_TO_ARRANGEMENT"` + +**Launch quantization values:** `"NONE"`, `"1/32"`, `"1/16"`, `"1/8"`, `"1/4"`, `"1/2"`, `"1"`, `"2"`, `"4"`, `"8"` + +--- + +### Track + +Tools for mixer controls, clips, sends, and navigation across the 8-track bank window. All `track_index` parameters are **1-indexed** (1–8). + +| Tool | Parameters | Description | +|---|---|---| +| `track_set_volume` | `track_index`, `value` (0–127) | Set track volume | +| `track_reset_volume` | `track_index` | Reset volume to unity gain | +| `track_set_pan` | `track_index`, `value` (0–127) | Set track pan (64 = center) | +| `track_reset_pan` | `track_index` | Reset pan to center | +| `track_toggle_mute` | `track_index`, `value?` | Toggle/set mute | +| `track_toggle_solo` | `track_index`, `value?` | Toggle/set solo | +| `track_toggle_recarm` | `track_index`, `value?` | Toggle/set record arm | +| `track_toggle_monitor` | `track_index`, `value?` | Toggle/set input monitoring | +| `track_toggle_auto_monitor` | `track_index`, `value?` | Toggle/set auto-monitoring | +| `track_select` | `track_index` | Select/focus a track | +| `track_set_crossfade_mode` | `track_index`, `mode` ("A"/"B"/"AB") | Set crossfade assignment | +| `track_set_send_volume` | `track_index`, `send_index`, `value` (0–127) | Set a send level | +| `track_reset_send_volume` | `track_index`, `send_index` | Reset send to default | +| `track_toggle_send` | `track_index`, `send_index`, `value?` | Toggle/set send active | +| `track_clip_launch` | `track_index`, `clip_index` | Launch a clip slot | +| `track_clip_launch_alt` | `track_index`, `clip_index` | Launch clip (alternative mode) | +| `track_clip_record` | `track_index`, `clip_index` | Record into a clip slot | +| `track_clip_create` | `track_index`, `clip_index`, `length_beats=4.0` | Create empty clip | +| `track_clip_duplicate` | `track_index`, `clip_index` | Duplicate a clip | +| `track_clip_remove` | `track_index`, `clip_index` | Delete a clip | +| `track_clip_set_name` | `track_index`, `clip_index`, `name` | Rename a clip | +| `track_clip_set_color` | `track_index`, `clip_index`, `color` | Set clip color (rgb string) | +| `track_navigate_next` | — | Select next track | +| `track_navigate_prev` | — | Select previous track | +| `track_bank_next` | — | Scroll track bank forward one page | +| `track_bank_prev` | — | Scroll track bank backward one page | +| `track_navigate_parent` | — | Navigate to parent group track | +| `track_enter_group` | `track_index` | Enter a group track | +| `track_add_audio` | — | Add a new audio track | +| `track_add_instrument` | — | Add a new instrument track | +| `track_add_effect` | — | Add a new effect/FX track | +| `track_stop_clips` | `track_index` | Stop all clips on a track | +| `track_toggle_bank` | — | Toggle between main and effect track bank | +| `track_set_param` | `param_index` (1–8), `value` (0.0–1.0) | Set a remote control parameter | +| `track_reset_param` | `param_index` | Reset remote control parameter | +| `track_navigate_params` | `direction` ("+"/"-"/"page+"/"page-") | Navigate remote control parameters | +| `track_indicate_volume` | `all_tracks=True` | Enable volume indication | +| `track_indicate_pan` | `all_tracks=True` | Enable pan indication | +| `track_indicate_send` | `send_index` | Enable send indication | +| `track_toggle_vu_meters` | `value?` | Toggle/set VU meter feedback | +| `track_get_info` | `track_index` | Get full state for one track | +| `track_get_all` | — | Get state for all 8 tracks in bank | +| `track_selected_get_state` | — | Get cursor (selected) track state | +| `track_selected_set_volume` | `value` (0–127) | Set cursor track volume | +| `track_selected_set_pan` | `value` (0–127) | Set cursor track pan | +| `track_selected_toggle_mute` | `value?` | Toggle/set cursor track mute | +| `track_selected_toggle_solo` | `value?` | Toggle/set cursor track solo | +| `track_selected_toggle_recarm` | `value?` | Toggle/set cursor track record arm | + +--- + +### Master Track + +| Tool | Parameters | Description | +|---|---|---| +| `master_set_volume` | `value` (0–127) | Set master track volume | +| `master_reset_volume` | — | Reset master volume to unity | +| `master_set_pan` | `value` (0–127) | Set master track pan | +| `master_reset_pan` | — | Reset master pan to center | +| `master_toggle_mute` | `value?` | Toggle/set master mute | +| `master_select` | — | Select/focus the master track | +| `master_get_state` | — | Get complete master track state | + +--- + +### Device + +Tools for the cursor device (the currently focused device in Bitwig's device chain). Also covers the primary instrument (`primary_*`) and EQ device (`eq_*`). + +| Tool | Parameters | Description | +|---|---|---| +| `device_toggle_bypass` | `value?` | Toggle/set device bypass | +| `device_toggle_expand` | `value?` | Toggle/set device expanded view | +| `device_toggle_parameters` | `value?` | Toggle/set parameters panel | +| `device_toggle_window` | `value?` | Toggle/set plugin window | +| `device_toggle_pinned` | `value?` | Toggle/set cursor pin | +| `device_navigate_next` | — | Move cursor to next device in chain | +| `device_navigate_prev` | — | Move cursor to previous device | +| `device_set_param` | `param_index` (1–8), `value` (0.0–1.0) | Set device parameter | +| `device_reset_param` | `param_index` | Reset parameter to default | +| `device_navigate_params` | `direction` ("+"/"-"/"page+"/"page-") | Navigate parameter pages | +| `device_select_page` | `page_index` | Select a parameter page | +| `device_select_sibling` | `sibling_index` | Select a sibling device in chain | +| `device_layer_select` | `layer_index` | Select a device layer | +| `device_layer_set_volume` | `layer_index`, `value` (0–127) | Set layer volume | +| `device_layer_set_pan` | `layer_index`, `value` (0–127) | Set layer pan | +| `device_layer_toggle_mute` | `layer_index`, `value?` | Toggle/set layer mute | +| `device_layer_set_send_volume` | `layer_index`, `send_index`, `value` (0–127) | Set layer send volume | +| `device_layer_enter` | `layer_index` | Enter a layer to view its devices | +| `device_layer_navigate` | `direction` ("+"/"-"/"page+"/"page-") | Navigate layers | +| `device_drumpad_select` | `pad_index` | Select a drum pad | +| `device_drumpad_set_volume` | `pad_index`, `value` (0–127) | Set drum pad volume | +| `device_drumpad_set_pan` | `pad_index`, `value` (0–127) | Set drum pad pan | +| `device_drumpad_toggle_mute` | `pad_index`, `value?` | Toggle/set drum pad mute | +| `device_drumpad_toggle_solo` | `pad_index`, `value?` | Toggle/set drum pad solo | +| `primary_set_param` | `param_index` (1–8), `value` (0.0–1.0) | Set primary instrument parameter | +| `primary_toggle_bypass` | `value?` | Toggle/set primary instrument bypass | +| `primary_navigate_params` | `direction` ("+"/"-"/"page+"/"page-") | Navigate primary instrument params | +| `eq_set_type` | `band_index`, `eq_type` (string) | Set EQ band filter type | +| `eq_set_gain` | `band_index`, `value` (0.0–1.0) | Set EQ band gain | +| `eq_set_frequency` | `band_index`, `value` (0.0–1.0) | Set EQ band frequency (normalized) | +| `eq_set_q` | `band_index`, `value` (0.0–1.0) | Set EQ band Q factor (normalized) | +| `eq_add` | — | Add an EQ+ device to the selected track | +| `device_get_state` | — | Get full device state (params, pages, layers, siblings) | + +**EQ band types:** `"OFF"`, `"LP1"`, `"LP2"`, `"HP1"`, `"HP2"`, `"LS"` (low shelf), `"HS"` (high shelf), `"BELL"`, `"NOTCH"` + +--- + +### Clip + +Tools for the **cursor clip** — the clip currently in focus in Bitwig's clip editor. + +| Tool | Parameters | Description | +|---|---|---| +| `clip_quantize` | — | Quantize the cursor clip | +| `clip_set_name` | `name` | Rename the cursor clip | +| `clip_set_color` | `color` (rgb string) | Set cursor clip color | +| `clip_toggle_pinned` | `value?` | Toggle/set clip pin (keeps focus on this clip) | +| `clip_launch` | — | Launch the cursor clip | +| `clip_launch_alt` | — | Launch cursor clip (alternative mode) | +| `clip_record` | — | Record into the cursor clip | +| `clip_create` | `length_beats=4.0` | Create a new empty clip | +| `clip_stop` | — | Stop clips on the cursor track | +| `clip_stop_alt` | — | Stop clips (alternative mode) | +| `clip_stop_all` | — | Stop all playing clips globally | +| `clip_stop_all_alt` | — | Stop all clips (alternative mode) | +| `clip_navigate` | `direction` ("+"/"-") | Navigate to next/previous clip | +| `clip_get_state` | — | Get cursor clip state (name, color, pinned) | + +--- + +### Scene + +Tools for the clip launcher scene bank. All `scene_index` parameters are **1-indexed**. + +| Tool | Parameters | Description | +|---|---|---| +| `scene_launch` | `scene_index` | Launch a scene (triggers all clips in that row) | +| `scene_launch_alt` | `scene_index` | Launch scene (alternative mode) | +| `scene_select` | `scene_index` | Select a scene | +| `scene_duplicate` | `scene_index` | Duplicate a scene | +| `scene_remove` | `scene_index` | Delete a scene | +| `scene_set_name` | `scene_index`, `name` | Rename a scene | +| `scene_set_color` | `scene_index`, `color` (rgb string) | Set scene color | +| `scene_navigate` | `direction` ("+"/"-"/"bank+"/"bank-") | Scroll scene bank | +| `scene_add` | — | Add a new empty scene | +| `scene_create_from_playing` | — | Create scene from all currently playing clips | +| `scene_get_all` | — | Get state of all 8 scenes in current bank | + +--- + +### Browser + +Tools for navigating Bitwig's content browser (presets, devices, samples). + +| Tool | Parameters | Description | +|---|---|---| +| `browser_open_preset` | — | Open browser to replace current device with a preset | +| `browser_insert_device_after` | — | Open browser to insert device after cursor | +| `browser_insert_device_before` | — | Open browser to insert device before cursor | +| `browser_navigate_tab` | `direction` ("+"/"-") | Navigate content type tabs | +| `browser_navigate_filter` | `column_index`, `direction` ("+"/"-") | Navigate a filter column | +| `browser_reset_filter` | `column_index` | Reset a filter column to show all | +| `browser_navigate_results` | `direction` ("+"/"-") | Navigate results list | +| `browser_commit` | — | Load the selected browser item | +| `browser_cancel` | — | Close browser without loading | +| `browser_get_state` | — | Get browser state (tab, filters, results) | + +--- + +### Project + +| Tool | Parameters | Description | +|---|---|---| +| `project_save` | — | Save the current project | +| `project_navigate` | `direction` ("+"/"-") | Switch between open projects | +| `project_toggle_engine` | `value?` | Toggle/set the Bitwig audio engine | +| `project_set_param` | `param_index` (1–8), `value` (0.0–1.0) | Set a project remote control parameter | +| `project_reset_param` | `param_index` | Reset project parameter to default | +| `project_navigate_params` | `direction` ("+"/"-"/"page+"/"page-") | Navigate project parameters | +| `project_select_page` | `page_index` | Select a project parameter page | +| `project_get_state` | — | Get project state (name, engine, params, pages) | + +--- + +### Layout & Panels + +Tools for controlling Bitwig's UI layout and panel visibility. + +| Tool | Parameters | Description | +|---|---|---| +| `layout_set` | `layout_name` | Set the panel layout (e.g. `"SINGLE"`, `"DOUBLE"`, `"THREE"`) | +| `panel_toggle_note_editor` | `value?` | Toggle/set note editor panel | +| `panel_toggle_automation_editor` | `value?` | Toggle/set automation editor panel | +| `panel_toggle_devices` | `value?` | Toggle/set devices panel | +| `panel_toggle_mixer` | `value?` | Toggle/set mixer panel | +| `panel_toggle_fullscreen` | `value?` | Toggle/set fullscreen mode | +| `arranger_toggle_cue_markers` | `value?` | Toggle cue marker visibility in arranger | +| `arranger_toggle_playback_follow` | `value?` | Toggle playback follow (auto-scroll) | +| `arranger_toggle_track_height` | `value?` | Toggle double track row height | +| `arranger_toggle_clip_launcher` | `value?` | Toggle clip launcher section in arranger | +| `arranger_toggle_timeline` | `value?` | Toggle timeline visibility | +| `arranger_toggle_io_section` | `value?` | Toggle I/O section in arranger | +| `arranger_toggle_effect_tracks` | `value?` | Toggle effect tracks visibility | +| `mixer_toggle_clip_launcher` | `value?` | Toggle clip launcher in mixer | +| `mixer_toggle_crossfade_section` | `value?` | Toggle crossfade section in mixer | +| `mixer_toggle_device_section` | `value?` | Toggle device section in mixer | +| `mixer_toggle_sends_section` | `value?` | Toggle sends section in mixer | +| `mixer_toggle_io_section` | `value?` | Toggle I/O section in mixer | +| `mixer_toggle_meter_section` | `value?` | Toggle meter/VU section in mixer | +| `layout_get_state` | — | Get full layout and visibility state | + +--- + +### MIDI (Virtual Keyboard) + +Tools for sending MIDI messages through DrivenByMoss's virtual keyboard. Channels are **1-indexed** (1–16). + +| Tool | Parameters | Description | +|---|---|---| +| `midi_note` | `channel`, `note` (0–127), `velocity` (0–127) | Send a note on/off message | +| `midi_note_with_array` | `channel`, `note`, `velocity` | Send note using array format | +| `midi_note_octave` | `channel`, `direction` ("+"/"-") | Shift note keyboard octave | +| `midi_drum` | `channel`, `note` (0–127), `velocity` (0–127) | Send a drum pad note | +| `midi_drum_octave` | `channel`, `direction` ("+"/"-") | Shift drum pad octave | +| `midi_cc` | `channel`, `cc` (0–127), `value` (0–127) | Send a MIDI CC message | +| `midi_channel_aftertouch` | `channel`, `pressure` (0–127) | Send channel aftertouch | +| `midi_poly_aftertouch` | `channel`, `note` (0–127), `pressure` (0–127) | Send polyphonic aftertouch | +| `midi_pitchbend` | `channel`, `value` (0–127) | Send pitch bend (64 = center) | +| `midi_set_fixed_velocity` | `velocity` (0–127) | Set fixed accent velocity (0 = disable) | +| `midi_note_repeat_toggle` | `value?` | Toggle/set note repeat mode | +| `midi_note_repeat_period` | `period` (string) | Set note repeat period (e.g. `"1/16"`) | +| `midi_note_repeat_length` | `length` (string) | Set note repeat length (e.g. `"1/16"`) | +| `midi_get_state` | — | Get MIDI/note repeat state | + +**Note repeat period/length values:** `"1/32"`, `"1/16"`, `"1/8T"`, `"1/8"`, `"1/4T"`, `"1/4"`, `"1/2"`, `"1"` + +--- + +### Miscellaneous + +| Tool | Parameters | Description | +|---|---|---| +| `undo` | — | Undo the last action | +| `redo` | — | Redo the last undone action | +| `action_execute` | `action_index` (1–20) | Execute a DrivenByMoss assignable custom action | +| `marker_launch` | `marker_index` | Jump to and play from a cue marker | +| `marker_navigate` | `direction` ("bank+"/"bank-") | Navigate marker bank pages | +| `marker_get_all` | — | Get all markers in current bank | +| `refresh_state` | — | Send `/refresh` and return full state dump | +| `state_dump` | — | Return current state store without refreshing | +| `ping_bitwig` | — | Check if Bitwig+DrivenByMoss is reachable | + +--- + +## Usage Examples + +### Check connection and get initial state + +``` +ping_bitwig +→ {"connected": true, "message": "Bitwig + DrivenByMoss OSC is active"} + +refresh_state +→ {"/play": (0,), "/tempo/raw": (128.0,), "/track/1/name": ("Drums",), ...} +``` + +### Transport control + +``` +transport_set_tempo(bpm=120.0) +transport_toggle_loop(value=1) +transport_play() + +transport_get_state() +→ {"playing": 1, "tempo_bpm": 120.0, "loop": 1, ...} + +transport_stop() +``` + +### Mixing + +``` +track_get_all() +→ [{"index": 1, "name": "Drums", "volume": 100, "mute": 0, ...}, ...] + +track_set_volume(track_index=1, value=90) +track_toggle_mute(track_index=2) +track_toggle_solo(track_index=1) + +master_set_volume(value=110) +``` + +### Launching clips and scenes + +``` +scene_get_all() +→ [{"index": 1, "name": "Verse", ...}, {"index": 2, "name": "Chorus", ...}] + +scene_launch(scene_index=2) + +track_clip_launch(track_index=3, clip_index=1) + +clip_stop_all() +``` + +### Device control + +``` +device_get_state() +→ {"name": "Polysynth", "bypass": 0, "params": [...], "pages": [...]} + +device_set_param(param_index=1, value=0.75) +device_select_page(page_index=2) +device_toggle_bypass() +``` + +### MIDI input + +``` +# Play a C major chord on channel 1 +midi_note(channel=1, note=60, velocity=100) # C4 +midi_note(channel=1, note=64, velocity=100) # E4 +midi_note(channel=1, note=67, velocity=100) # G4 + +# Release +midi_note(channel=1, note=60, velocity=0) +midi_note(channel=1, note=64, velocity=0) +midi_note(channel=1, note=67, velocity=0) + +# Drum hit on pad 1 (usually kick drum) +midi_drum(channel=10, note=36, velocity=127) +``` + +### Browser + +``` +browser_open_preset() # Opens browser for current device +browser_navigate_tab("+") # Switch to next content type +browser_navigate_results("+") # Move to next result +browser_commit() # Load the selection +``` + +--- + +## Troubleshooting + +### `ping_bitwig` returns `connected: false` + +1. **Is Bitwig running?** Make sure Bitwig Studio is open and a project is loaded. +2. **Is DrivenByMoss active?** Go to Preferences → Controllers and confirm the OSC controller is enabled (green checkmark). +3. **Check ports:** DrivenByMoss "receive port" must be `8000` and "send to port" must be `9000` (or set `BITWIG_SEND_PORT`/`BITWIG_RECEIVE_PORT` to match your custom values). +4. **Firewall:** Ensure UDP ports 8000 and 9000 are not blocked by a firewall. +5. **Port conflict:** Check nothing else is using those ports: `netstat -ano | findstr :8000` (Windows). + +### State values all return `None` + +The state store is empty, meaning no messages have been received from Bitwig. Call `refresh_state` to trigger a full dump: +``` +refresh_state() +``` +If it still returns mostly empty, see the connectivity steps above. + +### `track_get_all` shows only 8 tracks but I have more + +DrivenByMoss uses a **track bank** window of 8 tracks. Use `track_bank_next` / `track_bank_prev` to scroll to other groups of tracks. + +### Changes not reflected in state after a command + +DrivenByMoss sends state updates asynchronously. For fast sequences of commands, wait briefly or call `refresh_state` to re-sync. The `BITWIG_REFRESH_WAIT` env var controls how long `refresh()` waits (default 0.5 s). + +### Server exits immediately + +Make sure you are running the server directly, not piping to it. The MCP server communicates over stdio — it is meant to be launched by a MCP client (like Claude Code), not run interactively in a terminal. To test it manually, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). + +--- + +## License + +MIT