Skip to main content
Glama
itsnocny

lovense-mcp

by itsnocny

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.


Related MCP server: mcp-vnc

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

A
license - permissive license
-
quality - not tested
D
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/itsnocny/lovense-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server