Skip to main content
Glama
levinelson

bigskymx-mcp

by levinelson
README.md
# bigskymx-mcp

Control a Strymon BigSky MX over USB MIDI, from a command line or from Claude.

## Getting started

You need:

- [Nixie 2](https://www.strymon.net/support/nixie-2/), Strymon's free editor
- Python 3.10 or newer
- git

Developed and tested on macOS. It should work anywhere `python-rtmidi` does; on Linux that means ALSA development headers if pip has to build it from source.

### 1. Connect the pedal to your computer

### 2. Open Nixie 2 and leave it running

A freshly connected pedal sends no MIDI until an editor session exists. Nixie does not interfere with this tool.

### 3. Install

```bash
git clone https://github.com/levinelson/bigskymx-mcp.git
cd bigskymx-mcp
python3 -m venv .venv
.venv/bin/pip install -e ".[mcp]"
```

Drop `[mcp]` if you only want the command line. This puts `bigsky` in `.venv/bin`, not on your PATH; `uv tool install .` or `pipx install .` gets you a plain `bigsky` anywhere. Examples below use the bare name.

### 4. Check it works

```bash
.venv/bin/bigsky ports      # BigSkyMX should be listed and auto-detected
.venv/bin/bigsky state      # prints the loaded preset and both engines
```

| Symptom                                               | Cause or fix                  |
| ----------------------------------------------------- | ----------------------------- |
| `ports` does not list `BigSkyMX`                      | cable or connection           |
| `ports` finds it, `state` says it is not broadcasting | Nixie 2 is not open           |
| the stream stops after working                        | `bigsky preset 0` restarts it |
| still stopped with Nixie open                         | quit Nixie                    |

## Commands

### Everyday

```bash
bigsky ports                                # list MIDI ports, show auto-detect
bigsky state                                # read the loaded preset
bigsky state --config                       # read global settings
bigsky param TYPE NONLINEAR --engine 1      # switch reverb machine
bigsky param SHAPE BOUNCE --engine 1        # machine-specific, by name
bigsky param Decay 100 --engine 1           # 0-127, scaled to the full range
bigsky param DUAL "SERIES 1 >> 2"           # routing
bigsky param "INF MODE" INFINITE --engine 1
bigsky preset 42                            # recall a slot, 0-299
bigsky load "presets/PAD.json"              # restore a sound to the edit buffer
bigsky backup out/                          # every slot to JSON plus a .syx
bigsky machines                             # every machine and its parameters
bigsky monitor                              # print incoming MIDI
```

Choice parameters take names, not numbers. `--channel` defaults to `auto` and reads the channel off the pedal.

### Offline, no pedal needed

| Command                       | Does                        |
| ----------------------------- | --------------------------- |
| `bigsky show FILE.syx`        | decode a preset bank        |
| `bigsky export FILE.syx DIR/` | bank to editable JSON files |
| `bigsky build DIR/ FILE.syx`  | JSON files back to a bank   |
| `bigsky diff A.syx B.syx`     | byte-diff two banks         |

### Working on the protocol

| Command               | Does                                                   |
| --------------------- | ------------------------------------------------------ |
| `bigsky probe`        | sweep the SysEx opcode space (needs `--yes`)           |
| `bigsky dump`         | send one SysEx request, save the reply (needs `--yes`) |
| `bigsky discover-cc`  | rebuild the CC map by watching the pedal               |
| `bigsky verify-types` | walk every reverb machine                              |
| `bigsky emulate`      | impersonate the pedal to capture what Nixie sends      |
| `bigsky proxy`        | sit between Nixie and the pedal                        |

`scripts/full_review.py` sets every parameter of every machine on both engines and checks the pedal's report. It passes 299/303; the four failures are `INF MIX`, which has no CC.

## Use with Claude

```bash
claude mcp add bigsky -- /full/path/to/.venv/bin/bigsky-mcp
```

Claude Desktop, in `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "bigsky": {
      "command": "/full/path/to/.venv/bin/bigsky-mcp"
    }
  }
}
```

The path must be absolute. The client launches the server and does not inherit your shell.

| Tool                       | Does                                          | Read-only | Destructive                 |
| -------------------------- | --------------------------------------------- | --------- | --------------------------- |
| `bigsky_get_state`         | read the loaded preset and both engines       | yes       | no                          |
| `bigsky_list_machines`     | the 12 reverb machines                        | yes       | no                          |
| `bigsky_describe_machine`  | one machine's parameters, ranges and choices  | yes       | no                          |
| `bigsky_set_params`        | set any number of parameters on either engine | no        | no                          |
| `bigsky_recall_preset`     | recall a slot, 0-299                          | no        | yes, discards unsaved edits |
| `bigsky_list_preset_files` | saved sound files                             | yes       | no                          |
| `bigsky_load_preset_file`  | restore one to the edit buffer                | no        | yes, discards unsaved edits |
| `bigsky_save_preset_file`  | write the current sound to a file             | no        | yes, can replace a file     |

`bigsky_set_params` takes a whole change at once and applies it in the order the pedal requires:

1. Shared globals, because `DUAL` and `PERSIST` reload the preset
2. The machine type
3. That machine's own parameters
4. `OUTPUT LEVEL`, `PAN` and `INF MODE`, which survive a type change
5. The continuous knobs

`bigsky_save_preset_file` refuses to replace an existing file unless asked twice.

Set `BIGSKY_PRESETS` in the server's `env` to keep sounds somewhere other than `presets/`.

## What it cannot do

- **Store to a numbered slot.** I haven't found a MIDI message that does this. Dial a sound in and then save it manually on the pedal or in Nixie.
- **Set `INF MIX`.** It is in Strymon's schema and in no CC table.
- **Assign PARAM 1 and PARAM 2.** CC 19-22 drive whatever is already assigned; nothing addresses the assignment. Presets store it as a name, so it looks settable. `load` lists these separately from real failures.

## The CC map

Transcribed in `cc.py` from the manual (Rev B, pages 70-76). **Scaled** means CC 0-127 spans a wider internal range; **direct** means the CC value is the value and clamps at the maximum.

| Parameter            | Engine 1 | Engine 2 | Range | Kind                             |
| -------------------- | -------- | -------- | ----- | -------------------------------- |
| Type                 | 1        | 2        | 0-11  | direct, machine index            |
| Decay                | 3        | 4        | 0-127 | scaled, ms internally            |
| Pre-Delay            | 5        | 6        | 0-127 | scaled, ms internally            |
| Output Level         | 7        | 8        | 0-16  | direct                           |
| Pan                  | 9        | 10       | 0-16  | direct                           |
| Tone                 | 11       | 12       | 0-127 | scaled to 0-255                  |
| Mod                  | 13       | 14       | 0-127 | scaled to 0-255                  |
| Mix                  | 15       | 16       | 0-127 | scaled to 0-255                  |
| INF MODE             | 17       | 18       | 0-2   | direct, freeze / infinite / off  |
| Param1               | 19       | 20       | 0-127 | scaled, drives the assigned knob |
| Param2               | 21       | 22       | 0-127 | scaled, drives the assigned knob |
| Low End              | 23       | 24       | 0-20  | direct                           |
| MIDI Clock (Magneto) | 31       | 32       | 0-1   | direct                           |

Machine-specific parameters are in `cc.MACHINE_CC`.

| Shared control | CC  | Takes                                                                                 |
| -------------- | --- | ------------------------------------------------------------------------------------- |
| Boost          | 79  | 0-60                                                                                  |
| Persist        | 84  | `ON`, `OFF`                                                                           |
| INF LATCH      | 98  | `MOMENTARY`, `LATCHING`                                                               |
| DUAL           | 99  | `OFF`, `PARALLEL`, `SERIES 1 >> 2`, `SERIES 1 << 2`, `SPLIT 1L * 2R`, `SPLIT 1R * 2L` |

| Hardware control    | CC      | Notes                         |
| ------------------- | ------- | ----------------------------- |
| Bank up / down      | 80 / 81 | also the A and B footswitches |
| Infinite footswitch | 82      | acts as the physical switch   |
| **Value encoder**   | **83**  | **changes the loaded preset** |
| Infinite on / off   | 97      |                               |
| Expression          | 100     |                               |
| Tap                 | 101     |                               |
| Bypass              | 102     |                               |

Gotcha: sweeping CCs blindly hits 83 and silently swaps the preset out from under whatever you are testing.

Two corrections to the manual, both confirmed on hardware:

- **Persist is inverted.** The manual says 0 = Off. The pedal reports CC 84 = 0 as ON.
- **Shimmer Shift 2 is CC 73/74**, not the printed 72/73, which collides with Shift 1.

## The frame format

Presets and state are UTF-8 JSON inside SysEx.

```
F0 00 01 55 18 01 39 00 01 01 01 00 00 LL LL {"...json..."} F7
   |________| |  |  |  |  |  |         |___|
   Strymon    |  |  |  |  |  |         total length, 7-bit big endian
   mfr ID     |  |  |  |  |  payload type: 01=state 02=config 06=preset
              |  |  |  |  verb: 01=data 02=request 4A=select a slot
              |  |  |  7F on a preset, 00 on state/config
              |  |  opcode 0x39 = data   (0x43 = firmware update; avoid)
              |  device id 0x01
              product 0x18 = BigSky MX
```

Byte order is product, then device, then opcode.

**The JSON starts at byte 14, always.** Do not search for the first `{`: bytes 12-13 carry the length, and a low byte of `0x7B` is itself an ASCII `{`, so the search starts one byte early and the parse fails. This happens whenever the total length mod 128 is 123. Use `sysex.payload_json`.

**The two directions use different header lengths.** Pedal-to-host is 14 bytes before the JSON. Host-to-pedal is 10, with no length bytes: `00 01 55 18 01 39 00 01 01 01` then the JSON.

State frames can be partial, one parameter at a time:

```json
{ "state": { "channel": [{ "TYPE": "CLOUD", "LOW END": 10 }, {}] } }
```

## Opcodes

The pedal answers `0x46` to anything it does not implement.

| Opcode        | Reply         | What it is                                                  |
| ------------- | ------------- | ----------------------------------------------------------- |
| `0x21`        | 14 bytes      | Read ESN. Nixie's device probe.                             |
| `0x32`        | `00 01 00 00` | Unidentified. Version or capability word.                   |
| `0x39`        | _(silent)_    | Data. Silence means "send a payload".                       |
| `0x56`        | `0x45` (ACK)  | Accepted and executed, no front-panel change. Unidentified. |
| `0x00`        | _(silent)_    | Recognised, wants a payload.                                |
| anything else | `0x46`        | Not implemented.                                            |

The gen-1 command set is gone. These all NAK:

| Opcode | Was               |
| ------ | ----------------- |
| `0x62` | write patch       |
| `0x63` | request patch     |
| `0x64` | enter save mode   |
| `0x22` | get current state |

Only `0x21` carried over. All four are documented for TimeLine, Mobius and gen-1 BigSky in Strymon's own librarian, [strymon/spl](https://github.com/strymon/spl), in `spl/DcMidiDevDefs.h`.

`0x4A` selects a slot. It is not a save, and Nixie sends the identical message when you click a preset in its list.

## Gotchas

- The pedal ignores every message on a MIDI channel other than its own, with no error. A wrong channel looks exactly like a working connection.
- Writes go over CC. Sending the pedal its own state frame back also works and reaches full internal resolution, but stops applying after a while and silently reverts. `live.set_param` still exposes it.
- A JSON state write ignores a value of `0`, leaving the old value.
- Machine type cannot be changed by a JSON write. It needs CC 1/2.
- `DUAL` and `PERSIST` make the pedal reload the preset, reverting edits made before them. Set globals first.
- The pedal echoes what you sent before applying its own limits, so verifying too quickly reports success for a value that then gets clamped.
- Reading the stream without draining returns a stale snapshot.
- Re-sending a program change for the current preset does not reload it.
- Decay and Pre-Delay are milliseconds on a non-linear per-machine curve, so the CC value is searched for rather than computed. Mix, Tone and Mod land within one CC step of the request.

## Safety

Firmware updates use opcode `0x43`. Everything here stays on `0x39`, so none of it can trigger a flash. `probe` and `dump` can send undocumented opcodes and are gated behind `--yes`.

Run `bigsky backup out/` before experimenting.

## Your own sounds

`presets/` ships empty and is gitignored. Anything you put there is picked up by `bigsky load` and by the MCP preset tools.

## Not affiliated with Strymon

This is independent interoperability work, not authorized or endorsed by Strymon. Strymon, BigSky and Nixie are trademarks of their owner. Everything here came from observing a pedal I own and from the published manual, except `bigsky/schema.json`, which was derived from the Nixie 2 binary.

Written by Claude Code.

## License

MIT. See [LICENSE](LICENSE).