Skip to content

Gradleless/lovense-mcp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lovense-mcp

MCP server for controlling Lovense toys via the Lovense Cloud API. Runs on a home server, works from anywhere, lets your AI of choice take creative control.

Built with TypeScript, Bun, Hono, and the MCP SDK.


How it works

You (outside, Claude Desktop or any MCP client)
    ↕  HTTP/SSE — MCP transport
Your home server (NAS, VPS…)
    ↕  HTTPS — Lovense Server API
Lovense Cloud
    ↕  Push
Lovense Remote App (your phone, also outside)
    ↕  Bluetooth
Your toys

Why cloud and not LAN? The Lovense Remote app is on your phone, outside your home network — not on the same LAN as your server. The Lovense Server API (cloud relay) is therefore mandatory. Architecture fun, life complicated.

Why HTTP/SSE and not stdio? stdio requires the MCP server and the AI client to run on the same machine. Here the server is remote — hence HTTP/SSE transport.


Prerequisites

  • Bun installed on your server, you can use Node.js too.
  • A Lovense developer account with a developer token
  • The Lovense Remote app on your phone, logged in
  • At least one Lovense toy (this one's on you)
  • Your server must be publicly reachable (port forwarding, reverse proxy, etc.) so Lovense Cloud can send callbacks to it

Setup

1. Lovense Developer Portal

Follow Lovense's official guide to:

  • Register an application
  • Get your developer token
  • Set the callback URL to https://your-server.example.com/callback

The callback URL is the part everyone forgets. Without it, your server never hears from Lovense and nothing works. Don't skip this.

2. Install

git clone https://github.com/Gradleless/lovense-mcp
cd lovense-mcp
bun install

3. Configure

cp env.example .env

See Configuration below for all variables.

4. Run

# Development (watch mode, pretty logs)
bun run dev

# Production
bun run start

The server logs the setup URL on startup:

Setup page: http://localhost:3000/setup?token=your_token

5. Pair your toys

Open the setup URL in a browser, scan the QR code with the Lovense Remote app. The app will call your /callback endpoint and register your toys. From this point on, the server receives periodic heartbeats (~every 10s) to track what's connected.

The QR code expires after 4 hours. Visit /setup again to get a new one.

6. Connect your MCP client

Point any MCP-compatible client (Claude Desktop, Cursor, Zed, etc.) at https://your-server/mcp.

Note: MCP OAuth authentication is not implemented yet. Handle auth at the reverse proxy level instead (IP allowlist, VPN, etc.).


MCP Tools

Tool Description
get_qr Display the Lovense pairing QR code
get_toys List connected toys and connection status
play_action Single action: vibrate, rotate, thrust, etc.
stroke_action Stroke + Thrusting for stroker toys (Solace, etc.)
play_preset Built-in Lovense patterns (pulse, wave, fireworks, earthquake)
play_pattern Looping intensity cycle with custom waypoints
play_sequence Multi-phase timed sequence (A then B then C, non-looping)
stop Emergency stop — halts everything immediately

Your AI should always call get_toys first to check connectivity and see which actions each toy actually supports. You normally don't need to specify it. Sending a Rotate command to a toy that only vibrates ends in disappointment for everyone.


get_qr

Generates and displays the Lovense pairing QR code. Use this when get_toys returns no toys or lastSeenAt is null — the app isn't connected yet.

The user scans the QR code with the Lovense Remote app on their phone to establish the connection. After scanning, the server starts receiving heartbeats and toys become available.

No parameters. Returns the QR code as an image, plus a fallback URL if the image can't be displayed.

The QR code expires after 4 hours. Call get_qr again to get a fresh one.


get_toys

Returns the cached toy list from the last heartbeat. No API call to Lovense — just the server's in-memory state.

Output: { toys: [...], lastSeenAt: "ISO date or null" }

Each toy includes a features array listing its supported actions.

If lastSeenAt is null or more than 30 seconds old, the app is considered offline and any command will fail with a clear error message rather than silently doing nothing.


play_action

Single action for a given duration.

Parameter Type Description
type string Action type (see table below)
strength integer Intensity (range depends on action)
duration number Seconds. 0 = indefinite, otherwise ≥ 1
toyId string? Target a specific toy. Omit for all.

Action types and ranges:

Action Range Notes
Vibrate 0–20
Rotate 0–20
Thrusting 0–20
Fingering 0–20
Suction 0–20
Oscillate 0–20
All 0–20 Applies to all functions simultaneously
Pump 0–3 3 levels only
Depth 0–3 3 levels only

stroke_action

For stroker toys (Solace, etc.) that support the Stroke action. Only use if get_toys shows "Stroke" in the toy's features — otherwise you're just wishing.

Parameter Type Description
strokeMin integer Start position (0–100, 0 = fully retracted)
strokeMax integer End position (0–100, must be ≥ strokeMin + 20)
thrustingStrength integer Speed 0–20
duration number Seconds. 0 = indefinite, otherwise ≥ 1
toyId string? Target a specific toy. Omit for all.

Amplitude guide: very light = 0–20, light = 0–30, medium = 0–50, fairly strong = 0–65, strong = 0–80, max = 0–100.

For variable-intensity stroking patterns over time, use play_pattern instead.


play_preset

Plays a built-in Lovense pattern. Quick and easy when you don't want to think too hard.

Parameter Type Description
name string pulse / wave / fireworks / earthquake
duration number Seconds. 0 = indefinite, otherwise ≥ 1
toyId string? Target a specific toy. Omit for all.

play_pattern

Plays a smooth looping intensity cycle. The actions array defines one cycle that repeats for durationSec seconds (or indefinitely if 0). The server samples the waypoints at fixed intervals (max 50 steps, min 100ms each) and sends them to the app as a Pattern command — the app loops through them.

Parameter Type Description
actions array Cycle waypoints { ts, pos }, ordered by ascending ts
durationSec number Total run time in seconds. 0 = loop indefinitely until stop
toyId string? Target a specific toy. Omit for all.
  • ts: timestamp in milliseconds from cycle start (max 30,000 = 30s per cycle)
  • pos: intensity 0–100 (0 = off, 50 = medium, 100 = max)

The server interpolates linearly between waypoints when building the step sequence. For sharp transitions, place two points ~100ms apart.

For smooth looping: start and end the cycle at the same pos value (typically 0) so the loop restarts seamlessly.

Examples:

// Slow wave, loop forever
actions: [{ ts: 0, pos: 0 }, { ts: 3000, pos: 100 }, { ts: 6000, pos: 0 }]
durationSec: 0

// Ramp up over 5s then snap off, loop for 2 min
actions: [{ ts: 0, pos: 0 }, { ts: 5000, pos: 80 }, { ts: 5100, pos: 0 }]
durationSec: 120

// Fast pulse (on 500ms / off 500ms), 30s
actions: [{ ts: 0, pos: 100 }, { ts: 500, pos: 100 }, { ts: 600, pos: 0 }, { ts: 1000, pos: 0 }]
durationSec: 30

// Constant medium for 10s
actions: [{ ts: 0, pos: 50 }, { ts: 10000, pos: 50 }]
durationSec: 10

play_sequence

Executes a timed multi-phase sequence of steps. Returns immediately — scheduling runs server-side. Use this (not play_pattern) when:

  • Phases don't loop (A then B then C, done)
  • You switch feature types between phases (e.g. Vibrate 10s → Thrusting 10s)
  • You need multiple features at independent strengths simultaneously (e.g. Vibrate:15 + Rotate:8)

Each step has a duration (seconds) and either "Stop" or an array of simultaneous actions.

Parameter Type Description
steps array Ordered list of { duration, actions } steps
toyId string? Target a specific toy. Omit for all.

Returns { sequenceId, totalDuration }. Calling stop cancels any running sequence automatically.

Examples:

// Strong 5s → max 10s → pause 30s → medium 30s
[
  { duration: 5,  actions: [{ type: "Vibrate", strength: 16 }] },
  { duration: 10, actions: [{ type: "Vibrate", strength: 20 }] },
  { duration: 30, actions: "Stop" },
  { duration: 30, actions: [{ type: "Vibrate", strength: 10 }] },
]

// Vibrate + Rotate simultaneously at different strengths
[{ duration: 10, actions: [{ type: "Vibrate", strength: 15 }, { type: "Rotate", strength: 8 }] }]

// Vibrate phase then switch to Thrusting
[
  { duration: 20, actions: [{ type: "Vibrate",  strength: 13 }] },
  { duration: 20, actions: [{ type: "Thrusting", strength: 10 }] },
]

// Medium-amplitude thrust (Stroke toy): Stroke must always be paired with Thrusting
[{ duration: 10, actions: [{ type: "Stroke", min: 0, max: 50 }, { type: "Thrusting", strength: 10 }] }]

stop

Stops everything immediately. Cancels any running server-side sequence and sends a Function stop to the Lovense API, because when you want it to stop, you want it to stop.

Parameter Type Description
toyId string? Target a specific toy. Omit for all.

Intensity reference

Description 0–20 0–3 0–100
Very light 3 1 15
Light 6 1 30
Medium 10 2 50
Fairly strong 13 2 65
Strong 16 3 80
Max 20 3 100

Configuration

Variable Required Default Description
LOVENSE_TOKEN Developer token from the Lovense portal
LOVENSE_UID User identifier for your app
LOVENSE_UNAME Display name shown in Lovense Remote
LOVENSE_UTOKEN Arbitrary user token (any string you choose)
SETUP_TOKEN Protects the /setup page
MCP_PORT 3000 Port the server listens on
NODE_ENV production development enables pretty logs and debug level
LOG_LEVEL info trace / debug / info / warn / error / fatal

Tokens are never logged. Not even by accident.


HTTP endpoints

Endpoint Description
GET /mcp MCP transport (used by your AI client)
POST /callback Lovense heartbeat receiver
GET /setup?token=xxx QR code setup page
GET /health Health check → { ok: true }

Development

bun run dev        # watch mode, pretty logs, NODE_ENV=development
bun run start      # production
bun run typecheck  # type check only

Set LOG_LEVEL=debug to see full request/response bodies and AppState dumps. LOG_LEVEL=trace for internals.


Testing status

Honestly tested with actual hardware:

Feature Status
play_action → Vibrate ✅ Works
play_pattern, play_sequence, play_preset ✅ Works (vibration tested only)
get_toys, stop, get_qr ✅ Works
play_action → Rotate, Pump, Thrusting, etc. 🟡 Implemented per spec, untested
stroke_action 🟡 Implemented per spec, untested

The untested tools are implemented exactly according to the Lovense Server API docs and should work fine. I just don't own the entire Lovense catalog — I'm doing my best here, I assure you. PRs with real-world test reports are genuinely welcome.


License

MIT

About

Give Claude a very direct line to your Lovense toys. Science has gone far enough.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors