Dr. Dabber Switch 2 MCP
# Dr. Dabber Switch 2 MCP
A Model Context Protocol (MCP) server for controlling a Dr. Dabber Switch 2 vaporizer over Bluetooth LE. It exposes every safe user-facing control as a typed MCP tool, so any MCP-connected LLM or automation can discover, connect, and operate the device without touching the official app.
## Problem it solves
The Switch 2 ships with a mobile/web Bluetooth app. This server bridges the device into the MCP ecosystem so you can drive it from Claude, Codex, or any other MCP client on your laptop or over a Raspberry Pi BLE proxy, keeping the device always reachable even when your phone app is not connected.
---
## Install
### Option A: run from GitHub (no clone needed)
```bash
npx -y github:nidamen/drdabber-switch2-mcp
```
### Option B: local clone
```bash
git clone https://github.com/nidamen/drdabber-switch2-mcp.git
cd drdabber-switch2-mcp
npm install
npm run build
```
Node 20+ is required.
---
## MCP client configuration
### Direct BLE (laptop)
The server opens its own Bluetooth connection. The Switch 2 must be on, nearby, and not connected to another client.
**Claude Desktop / `claude_desktop_config.json`:**
```json
{
"mcpServers": {
"drdabber-switch2": {
"command": "npx",
"args": ["-y", "github:nidamen/drdabber-switch2-mcp"],
"env": {
"DRDABBER_SWITCH2_TARGET": "My Switch 2"
}
}
}
}
```
**Or with a local build:**
```json
{
"mcpServers": {
"drdabber-switch2": {
"command": "node",
"args": ["/path/to/drdabber-switch2-mcp/dist/src/index.js"],
"env": {
"DRDABBER_SWITCH2_TARGET": "My Switch 2"
}
}
}
}
```
### Pi BLE proxy (always-on headless path)
When `DRDABBER_PROXY_URL` is set, the MCP forwards every command to the web control running on the Pi instead of opening its own BLE link. The Pi becomes the single BLE owner and multiple clients can share it without GATT contention.
```json
{
"mcpServers": {
"drdabber-switch2": {
"command": "node",
"args": ["/path/to/drdabber-switch2-mcp/dist/src/index.js"],
"env": {
"DRDABBER_PROXY_URL": "http://192.168.1.44:4477"
}
}
}
}
```
**TOML style (Codex / Autohand):**
```toml
[mcp_servers.drdabber-switch2]
command = "node"
args = ["/path/to/drdabber-switch2-mcp/dist/src/index.js"]
env = { DRDABBER_SWITCH2_TARGET = "My Switch 2" }
```
### Environment variables
| Variable | Required | Description |
|---|---|---|
| `DRDABBER_SWITCH2_TARGET` | No | BLE id or exact advertised name to prefer during scan. Omit to connect to the first Switch 2 found. |
| `DRDABBER_PROXY_URL` | No | Base URL of the Pi web control (e.g. `http://192.168.1.44:4477`). When set, all commands are proxied; BLE is not opened locally. |
---
## Transport
The server uses **stdio** MCP transport. It does not open any network ports itself. BLE communication runs via `@abandonware/noble` on the host machine, or via HTTP POST to the Pi proxy when `DRDABBER_PROXY_URL` is set.
---
## Complete tool reference
### Connection
| Tool | Purpose | Parameters | Returns |
|---|---|---|---|
| `discover_switch2` | Scan for nearby Switch 2 devices advertising the `fee7` BLE service | `timeoutMs` (int, optional, 1000-60000) | Array of `{ id, name, rssi, services, connectable }` |
| `connect_switch2` | Connect to the Switch 2 and read device information | `target` (string, optional) BLE id or exact advertised name | `Switch2DeviceInfo`: `{ id, name, model, serial, hardwareRevision, softwareRevision, manufacturer, rssi }` |
| `disconnect_switch2` | Disconnect from the current Switch 2 | none | `{ disconnected: true }` |
### Device state
| Tool | Purpose | Parameters | Returns |
|---|---|---|---|
| `get_device_info` | Return cached model, serial, firmware, and manufacturer info | none | `Switch2DeviceInfo` |
| `get_status` | Sync clock, request analytics, and return decoded device status | `waitMs` (int, optional, 0-10000, default 3000) ms to wait for BLE notifications | Full `Switch2State`: status + presets + analytics + raw notifications |
| `get_presets` | Return cached presets for all 5 vapor profiles | none | Array of `Switch2Preset`: `{ preset, tempF, tempC, holdTime, heatingProfile, heatingProfileLabel, customProfile }` |
### Preset configuration
| Tool | Purpose | Parameters | Returns |
|---|---|---|---|
| `set_active_preset` | Switch the active vapor profile | `preset` (int, required, 1-5) | `Switch2Status` |
| `set_preset_temperature` | Set a preset's target temperature | `preset` (int, required, 1-5); `tempF` (int, optional, 250-650) OR `tempC` (int, optional, 121-343). At least one is required. | Updated `Switch2Preset` |
| `set_preset_hold_time` | Set a preset's hold time | `preset` (int, required, 1-5); `holdTime` (int, required, 10-90 seconds) | Updated `Switch2Preset` |
| `set_heating_profile` | Set a preset's heating curve | `preset` (int, required, 1-5); `heatingProfile` (int, required): 161 Steady, 177 Ascent, 193 Descent, 209 Valley, 225 Hill, 241 Custom | Updated `Switch2Preset` |
### Session control
| Tool | Purpose | Parameters | Returns |
|---|---|---|---|
| `start_session` | Start the active heating session | none | `Switch2Status` |
| `stop_session` | Stop the active heating session | none | `Switch2Status` |
| `extend_session` | Set the session extension value | `minutes` (int, required, 0-9) | `Switch2Status` |
### Device settings
| Tool | Purpose | Parameters | Returns |
|---|---|---|---|
| `set_light_mode` | Set the LED pattern | `lightMode` (int, required, 0-17). 0=Stealth, 1=Calm, 2=Purple, 3=Blue, 4=Cyan, 5=Green, 6=Yellow, 7=Orange, 8=Red, 9=Pink, 10=Cali Sunset, 11=Purple Haze, 12=Northern Lights, 13=Vegas Nights, 14=Blue Dream, 15=Strawberry Cough, 16=Florida Groves, 17=Lime Light | `Switch2Status` |
| `set_light_brightness` | Set LED brightness | `brightness` (int, required, 0-100) | `Switch2Status` |
| `set_temp_unit` | Set displayed temperature unit | `tempUnit` (string, required): `"F"` or `"C"` | `Switch2Status` |
| `set_haptic_feedback` | Enable or disable haptic feedback | `enabled` (boolean, required) | `Switch2Status` |
| `set_auto_shutoff` | Set idle auto-shutoff timer. Use 0 to disable. | `minutes` (int, required, 0-60) | `Switch2Status` |
| `set_device_name` | Rename the Switch 2 (persisted on device) | `name` (string, required, 1-29 printable ASCII chars 0x20-0x7E) | `Switch2DeviceInfo` |
### Cleaning
| Tool | Purpose | Parameters | Returns |
|---|---|---|---|
| `start_cleaning_assist` | Start the cleaning assist cycle | none | `Switch2Status` |
| `stop_cleaning_assist` | Stop the cleaning assist cycle | none | `Switch2Status` |
**Total: 21 tools.**
---
## Quick usage examples
**Check device state and battery:**
> "Connect to my Switch 2 and tell me the battery level, current temperature, and active preset."
The LLM calls `connect_switch2` then `get_status` and reports back.
**Change light and set temperature:**
> "Set preset 3 to 480F and switch the light to Purple Haze."
The LLM calls `set_preset_temperature` with `{ preset: 3, tempF: 480 }` and `set_light_mode` with `{ lightMode: 11 }`.
**Automated session:**
> "Start a session on preset 2, then stop it after I say done."
The LLM calls `set_active_preset` with `{ preset: 2 }`, then `start_session`, and waits for the next instruction to call `stop_session`.
---
## Desktop control panel
A local web control panel is included under `app/`. It reuses `Switch2Controller` directly (no MCP layer) and serves a single-page UI at `http://localhost:4477` over HTTP + Server-Sent Events. Controls mirror the official app: live temperature dial, preset editor, light mode and brightness, session start/stop/extend, cleaning assist, device rename, and all device settings.
```bash
# Real device
npm run app
# open http://localhost:4477
# Mock device (no hardware needed)
npm run app:mock
```
---
## BLE protocol notes
Primary control surface:
| Purpose | UUID |
|---|---|
| Demo service | `0000fee7-0000-1000-8000-00805f9b34fb` |
| Write characteristic | `0000fec1-0000-1000-8000-00805f9b34fb` |
| Notify characteristic | `0000fec2-0000-1000-8000-00805f9b34fb` |
| Device information service | `0000180a-0000-1000-8000-00805f9b34fb` |
Intentionally excluded UUIDs:
| Purpose | UUID |
|---|---|
| OTA service | `e5c7a653-7e52-4b06-af32-960455084ae3` |
| OTA write | `4cbbd40d-f1fa-4dbd-9bb5-36ec99d99a08` |
| OTA notify | `739cb17a-1aec-4232-a01a-a250d4c35f0d` |
---
## Safety boundaries
This server intentionally does NOT expose:
- Firmware update or OTA writes
- Factory reset
- Arbitrary raw BLE writes
All state-changing tools validate input ranges before writing to the device. These boundaries are permanent design decisions, not omissions.
---
## Verified device
Live verified on May 29, 2026:
| Field | Value |
|---|---|
| BLE name | `My Switch 2` |
| Advertised service | `fee7` |
| Model | `Switch 2` |
| Serial | `SN-EXAMPLE-0000` |
| Hardware | `V2.0.0` |
| Firmware | `SW2-260322-010` |
| Manufacturer | `Dr.Dabber` |
---
## Development
```bash
npm test # unit tests (21 tests)
npm run build # compile TypeScript
npm run qa # build + test
npm run probe # BLE scan (requires hardware)
npm run live:status -- "My Switch 2" # read-only live status
```
---
## License
MIT
TDQS
Scored across 21 tools
Most tools have clearly distinct purposes. However, set_active_preset, set_heating_profile, set_preset_hold_time, and set_preset_temperature all modify presets, potentially causing confusion about which preset is being affected and whether heating profile applies to active or specific preset.
All tools follow a consistent verb_noun pattern in snake_case (e.g., connect_switch2, get_device_info, set_light_mode). Naming is uniform and predictable across the entire set.
With 21 tools, the count is slightly above the typical 3-15 range, but each tool corresponds to a distinct device feature or operation, so it is reasonable for a feature-rich device controller.
The tool surface covers connection, presets, sessions, lighting, cleaning, and various settings. Minor gaps like a factory reset or firmware update might exist, but the set is largely comprehensive for day-to-day device control.