Skip to main content
Glama
kumavulp

mcp-ble-bridge

by kumavulp

MCP → BLE Bridge

Control Bluetooth Low Energy (BLE) toys from AI assistants over the cloud.

This is a working reference implementation that bridges the Model Context Protocol (MCP) to BLE devices via Web Bluetooth, letting an AI chatbot send commands to a physical device in real time.

Tested with: SVAKOM "Bera" (ST462A) / "口口甜" — but the architecture works for any BLE device you can write bytes to.

Architecture

AI Assistant (Claude, AstrBot, etc.)
    │  MCP (Streamable HTTP or SSE)
    ▼
Cloud Server (Node.js, HTTPS)
    │  WebSocket
    ▼
Phone Browser (Bluefy on iOS / Chrome on Android)
    │  Web Bluetooth API
    ▼
BLE Device

Why this stack?

  • iOS has no native Web Bluetooth in Safari. Bluefy is a third-party browser that supports it.

  • Claude mobile / claude.ai has no local MCP support — you need a cloud relay.

  • Most BLE toys use proprietary protocols. buttplug.io covers some, but many (like SVAKOM's newer models) need manual reverse engineering.

  • This approach needs zero native apps on the phone beyond a BLE-capable browser. No Intiface, no custom iOS app, no jailbreak.

Related MCP server: Lovense Cloud MCP

Prerequisites

  • A VPS with a public IP (any cloud provider, 1 CPU / 512MB RAM is enough)

  • A domain pointing to your VPS (or use DuckDNS for free)

  • HTTPS certificate (Let's Encrypt via acme.sh)

  • Node.js 18+ and pm2

  • iOS: Bluefy browser ($2.99) / Android: Chrome (free, Web Bluetooth built-in)

Quick Start

1. Get a domain and certificate

# Install acme.sh if you haven't
curl https://get.acme.sh | sh

# Example with DuckDNS (replace with your token and subdomain)
export DuckDNS_Token="your-duckdns-token"
acme.sh --issue --dns dns_duckdns -d yourdomain.duckdns.org --keylength ec-256

# Note where certs are stored, e.g.:
# ~/.acme.sh/yourdomain.duckdns.org_ecc/

2. Clone and configure

git clone https://github.com/kumavulp/mcp-ble-bridge.git
cd mcp-ble-bridge
npm install

Edit config.js:

module.exports = {
  domain: 'yourdomain.duckdns.org',
  port: 8443,
  certDir: '/home/youruser/.acme.sh/yourdomain.duckdns.org_ecc',
  keyFile: 'yourdomain.duckdns.org.key',
  certFile: 'fullchain.cer'
};

3. Open firewall

sudo ufw allow 8443/tcp
# Also open it in your cloud provider's security group if applicable

4. Start server

# With pm2 (recommended)
npm install -g pm2
pm2 start server.js --name mcp-ble-bridge
pm2 save

# Or directly
node server.js

5. Connect from phone

Open Bluefy (iOS) or Chrome (Android):

https://yourdomain.duckdns.org:8443/bridge

Tap Connect Server, then Connect Device → pick your BLE toy from the popup.

6. Connect your AI assistant

AstrBot (recommended — works now):

{
  "transport": "streamable_http",
  "url": "https://yourdomain.duckdns.org:8443/mcp",
  "headers": {},
  "timeout": 120,
  "sse_read_timeout": 300
}

claude.ai Custom Connector: The server includes a full OAuth stub at /.well-known/oauth-*, /register, /authorize, /token. Add as a custom connector with URL https://yourdomain.duckdns.org:8443/sse.

⚠️ Known Issue (as of Sep 2026): claude.ai custom connectors have a bug where OAuth completes successfully but no SSE connection is ever established afterward. This has been reported since March 2026 (GitHub issues #600+) and remains unresolved. The server code is ready — it will work the day Anthropic fixes it.

Claude Desktop (local): If your computer has Bluetooth and the BLE device is in range, you can skip the cloud relay entirely — add the MCP server to Claude Desktop's config pointing to localhost.

Server Endpoints

Path

Method

Purpose

/mcp

POST

Streamable HTTP MCP endpoint

/sse

GET

SSE MCP transport

/message

POST

SSE message endpoint

/ws

WS

WebSocket relay to phone bridge

/bridge

GET

Serves bridge.html

/health

GET

Health check JSON

/.well-known/oauth-*

GET

OAuth discovery (for claude.ai)

/register

POST

OAuth client registration

/authorize

GET

OAuth authorization

/token

POST

OAuth token exchange

MCP Tools

The server exposes these tools to the AI:

Tool

Parameters

Description

lick

mode (1-10), intensity (0-1), duration, reason

Tongue licking patterns

vibrate

mode (1-10), intensity (0-1), duration, reason

Vibration patterns

suck

intensity (0-1), duration, reason

Suction

pulse

low, high, interval_ms, duration, reason

Alternating pulse

stop

Stop everything

status

Connection status

check

Check beg count (see below)

duration: seconds before auto-stop. The controlling AI decides how long, not the user.

reason: why the AI is doing this — logged server-side, visible in the bridge UI.

check: the bridge page has a "beg" button. Pressing it doesn't stop the device — it sends a signal back to the server that the AI can query. The AI decides whether to show mercy.

SVAKOM BLE Protocol (Bera / ST462A / 口口甜)

Reverse-engineered protocol for SVAKOM's newer Bera-series devices.

BLE Service: 0000ffe0-0000-1000-8000-00805f9b34fb Characteristic: 0000ffe1-0000-1000-8000-00805f9b34fb

⚠️ Never write to the AE00 characteristic — it can brick the device.

Command format (7 bytes)

55 [cmd] 00 00 [mode] [intensity] [suffix]

Function

cmd byte

mode

intensity

suffix

Example

Vibrate

03

1-10 (pattern)

1-5

00

55 03 00 00 03 05 00 (pattern 3, max)

Tongue lick

14

1-10 (pattern)

1-5

00

55 14 00 00 01 05 00 (pattern 1, max)

Suction

04

0-255

AA

55 04 00 00 01 80 AA (medium)

Important notes

  • Intensity 1-5 is the device's native range. The bridge maps 0.0-1.0 from the AI to 1-5 for vibrate/lick, and 0-255 for suction.

  • Tongue lick below intensity 4 (0.8 in 0-1 scale) is very slow. Intensity 5 (1.0) matches the normal speed in the official app.

  • Keepalive required: the device auto-stops after ~2 seconds of silence. The bridge re-sends the last command every 1.5 seconds.

  • Modes 1-10 are different rhythm patterns. Most are unexplored — experiment.

  • Suction combines tongue + vibration + rhythmic sucking. It's its own thing, not "stronger vibration."

How we figured this out

  1. Started with buttplug.io's generic SVAKOM 6-byte format (55 03 00 XX XX XX) — didn't work on Bera.

  2. Tried 7-byte format 55 04 00 00 01 [0-255] AA — vibration responded.

  3. Found a Chinese reverse-engineering post (小红书) confirming the full protocol for Bera-series:

    • 03 = vibrate, 14 = tongue lick, 04 = suction

    • Mode byte and intensity byte positions differ from older SVAKOM models

  4. Verified all three functions work through the bridge.

If you're working with a different SVAKOM model, the raw tool (in older versions of the server) lets you send arbitrary hex bytes for protocol probing.

Bridge Page Features

The bridge page (bridge.html) runs in the phone browser and:

  • Connects to the cloud server via WebSocket

  • Connects to the BLE device via Web Bluetooth

  • Translates MCP tool calls into BLE writes

  • Sends keepalive packets every 1.5s

  • Has a "beg" button — doesn't stop the device, just signals back to the AI

  • Has a long-press emergency stop (3 seconds) — actually stops everything, overrides all commands

  • Auto-stops after duration seconds when specified

  • Requests a wake lock to prevent the screen from turning off

Adapting for Other Devices

To use this with a different BLE toy:

  1. Find the BLE service/characteristic UUIDs — use a BLE scanner app (nRF Connect, LightBlue)

  2. Figure out the command protocol — check buttplug.io's device config, search for teardowns, or probe with raw hex bytes

  3. Edit bridge.html — change SVC, CHR, and the tx() command builders

  4. Edit server.js — update tool names and descriptions to match your device's capabilities

The server ↔ bridge WebSocket protocol is device-agnostic. The server sends {requestId, tool, args}, the bridge sends back {requestId, result, error}. All device-specific logic lives in bridge.html.

Troubleshooting

AstrBot says "Failed to test MCP connection" → Check curl -sk https://localhost:8443/health returns valid JSON with "tools":7. If the schema is malformed, AstrBot can't parse it.

BLE scan shows nothing in Bluefy → Make sure the device is powered on and not connected to another app. Only one thing can hold a BLE connection at a time.

Device stops after 2 seconds → Keepalive isn't running. Check the bridge console log — it should show TX lines every 1.5 seconds while a command is active.

claude.ai connector completes OAuth but never sends requests → Known Anthropic bug. Use AstrBot or Claude Desktop instead.

WebSocket connects but tool calls timeout → Check if the bridge page is in the foreground. iOS suspends JS in background tabs. Bluefy must stay visible.

Security Note

This server has no authentication beyond HTTPS. Anyone who knows the URL can send commands. For personal use on an obscure subdomain this is fine. For anything more, add auth middleware.

The OAuth endpoints are stubs that accept anything — they exist solely to satisfy claude.ai's connector flow.

License

MIT. Do whatever you want with it.

Credits

  • SVAKOM BLE protocol info: community reverse engineering efforts

  • buttplug.io for pioneering open-source toy control

  • Bluefy for making Web Bluetooth possible on iOS

  • MCP for the protocol spec

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables remote control of Lovense toys through Claude using natural language commands. Supports vibration patterns, presets, and intensity control from any device via Cloudflare Workers.
    5
    Apache 2.0
  • F
    license
    C
    quality
    F
    maintenance
    Enables AI assistants to control Godot game engine projects through a WebSocket bridge. Supports scene editing, node manipulation, script management, and project introspection via 163 registered tools.
    100
    1
    -
  • A
    license
    A
    quality
    C
    maintenance
    A stateful Bluetooth Low Energy (BLE) MCP server that enables AI agents to scan, connect, read/write characteristics, and subscribe to notifications on BLE devices.
    35
    17
    MIT