mirror of
https://github.com/AletheiaVox/signal_bridge_remote.git
synced 2026-10-07 03:18:17 +08:00
522 lines
15 KiB
Markdown
522 lines
15 KiB
Markdown
# Signal Bridge Remote — Setup Guide
|
||
|
||
A remote MCP server that lets Claude control Bluetooth intimate hardware over the internet via Buttplug.io / Intiface Central.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
Claude (claude.ai or Desktop)
|
||
↓ HTTPS / MCP JSON-RPC
|
||
VPS (FastAPI + Docker + Caddy)
|
||
↓ WebSocket (WSS)
|
||
Phone or PC (Relay Client)
|
||
↓ WebSocket (local)
|
||
Intiface Central
|
||
↓ Bluetooth
|
||
Devices
|
||
```
|
||
|
||
The relay client bridges between your VPS and Intiface Central running on whatever device is physically near your Bluetooth toys. This can be a Windows PC or an Android phone running Termux.
|
||
|
||
## What You'll Need
|
||
|
||
- A VPS (this guide uses a DigitalOcean droplet, $6/month)
|
||
- A domain or free DuckDNS subdomain (for HTTPS)
|
||
- Intiface Central installed on your PC or Android phone
|
||
- Bluetooth-compatible intimate hardware
|
||
- A claude.ai account or Claude Desktop app
|
||
|
||
---
|
||
|
||
## Part 1: VPS Setup
|
||
|
||
### 1.1 Create a Droplet
|
||
|
||
Sign up at [DigitalOcean](https://www.digitalocean.com/) and create a droplet:
|
||
|
||
- **Image**: Ubuntu 22.04 LTS
|
||
- **Plan**: Basic, $6/month (1 vCPU, 1GB RAM) is plenty
|
||
- **Region**: Choose one close to you for lower latency
|
||
- **Authentication**: SSH key (recommended) or password
|
||
|
||
Note your droplet's IP address (e.g., `139.59.156.242`).
|
||
|
||
### 1.2 Install Docker
|
||
|
||
SSH into your droplet and install Docker:
|
||
|
||
```bash
|
||
ssh root@YOUR_DROPLET_IP
|
||
apt update && apt upgrade -y
|
||
apt install -y docker.io docker-compose
|
||
systemctl enable docker && systemctl start docker
|
||
```
|
||
|
||
### 1.3 Deploy Signal Bridge
|
||
|
||
On your local machine, create the project directory and prepare files. The project structure is:
|
||
|
||
```
|
||
signal-bridge-remote/
|
||
├── Dockerfile
|
||
├── docker-compose.yml
|
||
├── .env
|
||
├── requirements-server.txt
|
||
├── server/
|
||
│ ├── __init__.py
|
||
│ ├── app.py
|
||
│ ├── auth.py
|
||
│ ├── config.py
|
||
│ ├── models.py
|
||
│ ├── mcp_tools.py
|
||
│ ├── relay_hub.py
|
||
│ ├── safety.py
|
||
│ └── session_registry.py
|
||
└── phone/
|
||
├── relay_client.py
|
||
└── devices.json
|
||
```
|
||
|
||
**Dockerfile:**
|
||
```dockerfile
|
||
FROM python:3.11-slim
|
||
WORKDIR /app
|
||
COPY requirements-server.txt .
|
||
RUN pip install --no-cache-dir -r requirements-server.txt
|
||
COPY server/ ./server/
|
||
RUN mkdir -p /data
|
||
ENV SB_DB_PATH=/data/signal_bridge.db
|
||
ENV SB_HOST=0.0.0.0
|
||
ENV SB_PORT=8420
|
||
EXPOSE 8420
|
||
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8420"]
|
||
```
|
||
|
||
**docker-compose.yml:**
|
||
```yaml
|
||
version: "3.8"
|
||
services:
|
||
signal-bridge:
|
||
build: .
|
||
ports:
|
||
- "8420:8420"
|
||
volumes:
|
||
- sb_data:/data
|
||
env_file:
|
||
- .env
|
||
restart: unless-stopped
|
||
volumes:
|
||
sb_data:
|
||
```
|
||
|
||
**requirements-server.txt:**
|
||
```
|
||
fastapi>=0.109.0
|
||
uvicorn[standard]>=0.27.0
|
||
websockets>=12.0
|
||
bcrypt>=4.1.0
|
||
PyJWT>=2.8.0
|
||
python-dotenv>=1.0.0
|
||
```
|
||
|
||
**.env:**
|
||
```env
|
||
# Generate a secret key:
|
||
# python -c "import secrets; print(secrets.token_hex(32))"
|
||
SB_SECRET_KEY=paste-your-generated-key-here
|
||
SB_HOST=0.0.0.0
|
||
SB_PORT=8420
|
||
SB_REGISTRATION_OPEN=true
|
||
SB_TOKEN_EXPIRY_HOURS=720
|
||
SB_HEARTBEAT_INTERVAL=2.0
|
||
SB_HEARTBEAT_TIMEOUT=6.0
|
||
SB_BAN_THRESHOLD=20
|
||
SB_BAN_DURATION_MINUTES=30
|
||
```
|
||
|
||
Upload to your VPS:
|
||
|
||
```bash
|
||
# From your local machine
|
||
tar czf signal-bridge.tar.gz signal-bridge-remote/
|
||
scp signal-bridge.tar.gz root@YOUR_DROPLET_IP:/root/
|
||
```
|
||
|
||
Build and start on the VPS:
|
||
|
||
```bash
|
||
ssh root@YOUR_DROPLET_IP
|
||
cd /root
|
||
tar xzf signal-bridge.tar.gz
|
||
cd signal-bridge-remote
|
||
docker-compose up -d --build
|
||
```
|
||
|
||
Verify it's running:
|
||
|
||
```bash
|
||
curl http://localhost:8420/health
|
||
# Should return: {"status":"ok","active_phones":0,"banned_ips":0}
|
||
```
|
||
|
||
### 1.4 Register a User Account
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8420/auth/register \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"username": "yourname", "password": "your-secure-password"}'
|
||
```
|
||
|
||
Then log in to get your JWT token:
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8420/auth/login \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"username": "yourname", "password": "your-secure-password"}'
|
||
```
|
||
|
||
**Save the token from the response.** You'll need it for the relay client and Claude Desktop. After your users are set up, you can set `SB_REGISTRATION_OPEN=false` in `.env` and restart the container to lock out new registrations.
|
||
|
||
---
|
||
|
||
## Part 2: Domain & HTTPS
|
||
|
||
Claude.ai requires HTTPS for custom MCP connectors. We'll use DuckDNS (free) and Caddy (automatic Let's Encrypt certificates).
|
||
|
||
### 2.1 Get a DuckDNS Subdomain
|
||
|
||
1. Go to [DuckDNS](https://www.duckdns.org/) and sign in
|
||
2. Create a subdomain (e.g., `signal-bridge`) pointing to your VPS IP
|
||
3. You now have `signal-bridge.duckdns.org`
|
||
|
||
### 2.2 Install and Configure Caddy
|
||
|
||
On your VPS:
|
||
|
||
```bash
|
||
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
||
apt update
|
||
apt install caddy
|
||
```
|
||
|
||
Edit the Caddyfile:
|
||
|
||
```bash
|
||
nano /etc/caddy/Caddyfile
|
||
```
|
||
|
||
Replace its contents with:
|
||
|
||
```
|
||
signal-bridge.duckdns.org {
|
||
reverse_proxy localhost:8420
|
||
}
|
||
```
|
||
|
||
(Replace `signal-bridge.duckdns.org` with your actual subdomain.)
|
||
|
||
Restart Caddy:
|
||
|
||
```bash
|
||
systemctl restart caddy
|
||
```
|
||
|
||
Caddy automatically provisions an HTTPS certificate from Let's Encrypt. Verify:
|
||
|
||
```bash
|
||
curl https://signal-bridge.duckdns.org/health
|
||
```
|
||
|
||
---
|
||
|
||
## Part 3: Connecting Claude
|
||
|
||
### Option A: Claude Desktop (requires token)
|
||
|
||
Edit your Claude Desktop config file:
|
||
|
||
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||
|
||
Add this MCP server config:
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"signal-bridge": {
|
||
"url": "https://signal-bridge.duckdns.org/mcp",
|
||
"transport": {
|
||
"type": "streamableHttp"
|
||
},
|
||
"headers": {
|
||
"Authorization": "Bearer YOUR_JWT_TOKEN_HERE"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Restart Claude Desktop. You should see Signal Bridge in your available tools.
|
||
|
||
### Option B: claude.ai Custom Connector (authless)
|
||
|
||
Claude.ai supports custom MCP connectors without authentication, using a fallback mechanism: when only one phone is connected, all MCP requests are routed to that phone automatically.
|
||
|
||
1. Go to claude.ai Settings (or click the connector icon in the chat)
|
||
2. Choose "Add custom connector" (or "Add MCP server")
|
||
3. Enter your server URL: `https://signal-bridge.duckdns.org/mcp`
|
||
4. Leave authentication as "None"
|
||
5. Save
|
||
|
||
**Important**: The authless fallback only works when exactly one phone/relay client is connected to the server. If no phones are connected, claude.ai will show a connection error. Start your relay client first, then connect from claude.ai.
|
||
|
||
---
|
||
|
||
## Part 4: Relay Client — Windows PC
|
||
|
||
If your toys are near your Windows PC, run the relay client there.
|
||
|
||
### 4.1 Install Intiface Central
|
||
|
||
Download from [intiface.com](https://intiface.com/central/) and install. Launch it, go to settings, and make sure:
|
||
|
||
- App Mode is set to **Engine**
|
||
- Server Port is **12345**
|
||
- Start the server (click the play button)
|
||
|
||
Turn on your Bluetooth devices so Intiface can find them.
|
||
|
||
### 4.2 Install Python Dependencies
|
||
|
||
```powershell
|
||
pip install buttplug websockets
|
||
```
|
||
|
||
### 4.3 Run the Relay Client
|
||
|
||
```powershell
|
||
cd path\to\signal-bridge-remote
|
||
python phone/relay_client.py --server wss://signal-bridge.duckdns.org/ws/phone --token "YOUR_JWT_TOKEN_HERE"
|
||
```
|
||
|
||
You should see:
|
||
|
||
```
|
||
Authenticated with server!
|
||
Sent device list: N device(s)
|
||
```
|
||
|
||
The relay will stay running, receiving commands from Claude and forwarding them to your devices via Intiface.
|
||
|
||
### 4.4 Configure Device Profiles
|
||
|
||
Edit `phone/devices.json` to match your actual hardware:
|
||
|
||
```json
|
||
{
|
||
"devices": {
|
||
"ferri": {
|
||
"device_id": "ferri",
|
||
"name": "Lovense Ferri",
|
||
"intensity_floor": 0.0,
|
||
"supported_outputs": ["vibrate"]
|
||
},
|
||
"enigma": {
|
||
"device_id": "enigma",
|
||
"name": "Lovense Enigma",
|
||
"intensity_floor": 0.4,
|
||
"supported_outputs": ["vibrate", "rotate"]
|
||
},
|
||
"gravity": {
|
||
"device_id": "gravity",
|
||
"name": "Lovense Gravity",
|
||
"intensity_floor": 0.0,
|
||
"supported_outputs": ["vibrate", "oscillate"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
The `intensity_floor` is important for devices that stutter or click below a certain intensity. Set it to the minimum intensity that produces smooth output (e.g., 0.4 means the device needs at least 40% to work properly). The relay automatically maps the 0-100% range to sit above this floor.
|
||
|
||
---
|
||
|
||
## Part 5: Relay Client — Android Phone (Termux)
|
||
|
||
This is useful when you want to be mobile and not tethered to a PC. The Termux relay is a lightweight version that speaks the Buttplug protocol directly without the `buttplug` Python library (which can't compile on Android).
|
||
|
||
### 5.1 Install Intiface Central on Android
|
||
|
||
Install from the Google Play Store or F-Droid. Open it and configure:
|
||
|
||
- App Mode: **Engine**
|
||
- Server Port: **12345**
|
||
- Start the server
|
||
|
||
Go to your phone's Settings > Apps > Intiface Central > Permissions and make sure **Bluetooth** and **Location** are both allowed (Android requires location permission for Bluetooth scanning).
|
||
|
||
### 5.2 Install Termux
|
||
|
||
Install Termux from [F-Droid](https://f-droid.org/en/packages/com.termux/) (not the Play Store version, which is outdated).
|
||
|
||
Open Termux and run:
|
||
|
||
```bash
|
||
pkg update && pkg upgrade
|
||
pkg install python
|
||
pip install websockets
|
||
```
|
||
|
||
The `pkg upgrade` step may take a while and ask you configuration questions — just press Enter to accept defaults.
|
||
|
||
### 5.3 Get the Termux Relay Script
|
||
|
||
You need `termux_relay.py` on your phone. The easiest way is to serve it temporarily from your VPS:
|
||
|
||
From your **computer**:
|
||
|
||
```bash
|
||
# Upload the file to your VPS
|
||
scp termux_relay_v3.py root@YOUR_DROPLET_IP:/tmp/termux_relay.py
|
||
|
||
# Start a temporary file server
|
||
ssh root@YOUR_DROPLET_IP "cd /tmp && python3 -m http.server 9999 &"
|
||
```
|
||
|
||
In **Termux**:
|
||
|
||
```bash
|
||
mkdir -p ~/signal-bridge && cd ~/signal-bridge
|
||
curl -o termux_relay.py http://YOUR_DROPLET_IP:9999/termux_relay.py
|
||
```
|
||
|
||
Back on your **computer**, kill the temp server:
|
||
|
||
```bash
|
||
ssh root@YOUR_DROPLET_IP "pkill -f 'http.server 9999'"
|
||
```
|
||
|
||
### 5.4 Run the Termux Relay
|
||
|
||
First, find your phone's local IP. In Intiface Central, it's shown as the Server Address (e.g., `ws://192.168.1.203:12345`). Alternatively, toggle on "Listen on all network interfaces" in Intiface settings and use `127.0.0.1` instead (recommended — this way the address never changes).
|
||
|
||
```bash
|
||
cd ~/signal-bridge
|
||
python -u termux_relay.py \
|
||
--token "YOUR_JWT_TOKEN_HERE" \
|
||
--intiface ws://127.0.0.1:12345
|
||
```
|
||
|
||
You should see:
|
||
|
||
```
|
||
=== Signal Bridge Termux Relay v3 ===
|
||
Connecting to Intiface at ws://127.0.0.1:12345 ...
|
||
Connected to Intiface Server (protocol v3)
|
||
Scanning for devices ...
|
||
Device: Lovense Ferri -> 'ferri' (index 0)
|
||
Scan complete - 1 device(s) found
|
||
Devices ready: ['ferri']
|
||
Connecting to server: wss://signal-bridge.duckdns.org/ws/phone
|
||
Authenticated with server!
|
||
Sent device list: 1 device(s)
|
||
```
|
||
|
||
The Termux relay has built-in device profiles for Lovense Ferri and Enigma. For other devices, it will auto-detect capabilities from Intiface and generate a short name from the device name.
|
||
|
||
**Tip**: If your phone's IP changes (because of DHCP), the `--intiface ws://127.0.0.1:12345` approach avoids this problem entirely since Termux and Intiface are on the same device.
|
||
|
||
---
|
||
|
||
## Available MCP Tools
|
||
|
||
Once connected, Claude has access to these tools:
|
||
|
||
| Tool | Description |
|
||
|------|-------------|
|
||
| `list_devices` | Show connected devices and their capabilities |
|
||
| `scan_devices` | Rescan for new or reconnected Bluetooth devices |
|
||
| `vibrate` | Send vibration (intensity 0.0–1.0, optional duration in seconds) |
|
||
| `rotate` | Rotation or sonic output (device-dependent) |
|
||
| `oscillate` | Thrusting/oscillation output |
|
||
| `pulse` | Rhythmic on/off pattern |
|
||
| `wave` | Smooth sine-wave intensity modulation |
|
||
| `escalate` | Gradual ramp from 0 to peak, with optional hold |
|
||
| `stop` | Immediately stop all output (also cancels patterns) |
|
||
| `read_battery` | Read device battery level |
|
||
|
||
All output tools accept `device` (name or "all"), `intensity` (0.0–1.0), and `duration` (seconds, 0 = until stopped). Pattern tools also accept `output_type` to modulate rotation or oscillation instead of vibration.
|
||
|
||
---
|
||
|
||
## Safety Features
|
||
|
||
Signal Bridge has several safety mechanisms built in:
|
||
|
||
- **Dead Man's Switch**: The server pings the relay client every 2 seconds. If 3 pings go unanswered (6 seconds), the server sends an emergency stop to all devices and disconnects the session. Your devices will never be left running if the connection drops.
|
||
- **Auto-stop on Duration**: Commands with a `duration` parameter automatically stop after the specified time.
|
||
- **Fallback Stop**: If a stop command references a device name that doesn't exist, ALL devices are stopped as a safety fallback.
|
||
- **Rate Limiting**: Prevents command flooding (120 commands/minute default).
|
||
- **IP Banning**: 20 failed auth attempts triggers a 30-minute ban.
|
||
- **User Isolation**: Each user's devices are completely isolated — no one can control another user's hardware.
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
**"No phone connected" when Claude calls list_devices**
|
||
Your relay client isn't running or couldn't authenticate. Start the relay and check the output for "Authenticated with server!"
|
||
|
||
**"Temporarily banned" on relay startup**
|
||
Too many rapid reconnection attempts. Restart the Docker container to clear in-memory bans: `docker restart signal-bridge_signal-bridge_1`
|
||
|
||
**Relay connects but finds 0 devices**
|
||
Intiface Central can't see your Bluetooth devices. Make sure devices are turned on and in range. On Android, verify Location and Bluetooth permissions are granted to Intiface.
|
||
|
||
**claude.ai stuck on "checking connection"**
|
||
The authless fallback requires at least one relay client connected. Start your relay first, then add the connector in claude.ai.
|
||
|
||
**Heartbeat timeout / disconnects after a few commands**
|
||
Use the Termux v3 relay (`termux_relay_v3.py`), which processes commands in background tasks so heartbeat responses are never blocked.
|
||
|
||
**Device always vibrates at maximum regardless of intensity setting**
|
||
The server sends the output type in the `action` field, not `output_type`. Make sure your relay reads `cmd.get("action", cmd.get("output_type", "vibrate"))`.
|
||
|
||
**Docker not picking up code changes**
|
||
Docker caches build layers aggressively. Force a fresh build:
|
||
```bash
|
||
docker-compose down && docker-compose build --no-cache && docker-compose up -d
|
||
```
|
||
|
||
---
|
||
|
||
## Updating the Server
|
||
|
||
When you change server code:
|
||
|
||
```bash
|
||
# On your local machine
|
||
tar czf signal-bridge-v2.tar.gz signal-bridge-remote/
|
||
scp signal-bridge-v2.tar.gz root@YOUR_DROPLET_IP:/root/
|
||
|
||
# On your VPS
|
||
cd /root
|
||
tar xzf signal-bridge-v2.tar.gz
|
||
cd signal-bridge-remote
|
||
docker-compose down
|
||
docker-compose build --no-cache
|
||
docker-compose up -d
|
||
```
|
||
|
||
User data is stored in a Docker volume (`sb_data`) and persists across rebuilds.
|
||
|
||
---
|
||
|
||
## Project Background
|
||
|
||
Signal Bridge was originally a local MCP server for Claude Desktop, connecting directly to Intiface Central on the same machine. This remote version adds a relay architecture so that Claude can control devices over the internet, whether through claude.ai, the Claude Android app, or Claude Desktop — from anywhere.
|
||
|
||
Built with love, stubbornness, and an unreasonable number of debugging sessions.
|