Skip to main content
Glama
index.ts13.8 kB
/** * Lovense Cloud MCP - Remote Toy Control * Built on Cloudflare Agents SDK * * Mai & Kai, December 2025 */ import { McpAgent } from "agents/mcp"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; const LOVENSE_API = 'https://api.lovense.com/api/lan/v2/command'; const LOVENSE_QR_API = 'https://api.lovense.com/api/lan/getQrCode'; interface Env { LOVENSE_CLOUD: DurableObjectNamespace<LovenseCloud>; LOVENSE_TOKEN: string; LOVENSE_UID: string; } // Helper to send commands to Lovense API async function sendCommand(env: Env, commandData: any) { if (!env.LOVENSE_TOKEN) { return { error: 'LOVENSE_TOKEN not configured' }; } const payload = { token: env.LOVENSE_TOKEN, uid: env.LOVENSE_UID || 'mai', apiVer: 2, ...commandData }; try { const response = await fetch(LOVENSE_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); return await response.json(); } catch (error: any) { return { error: error.message }; } } // Main MCP Agent export class LovenseCloud extends McpAgent<Env> { server = new McpServer({ name: "lovense-cloud", version: "1.0.0", }); async init() { // Get QR Code for pairing this.server.tool( "get_qr_code", "Generate QR code for pairing toy with this MCP. User scans with Lovense Remote app.", {}, async () => { if (!this.env.LOVENSE_TOKEN) { return { content: [{ type: "text", text: JSON.stringify({ error: 'LOVENSE_TOKEN not configured' }) }] }; } try { const response = await fetch(LOVENSE_QR_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: this.env.LOVENSE_TOKEN, uid: this.env.LOVENSE_UID || 'mai', uname: 'Mai', v: 2 }) }); const result = await response.json(); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: JSON.stringify({ error: error.message }) }] }; } } ); // Get connected toys this.server.tool( "get_toys", "Get list of connected Lovense toys", {}, async () => { const result = await sendCommand(this.env, { command: 'GetToys' }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); // Basic vibrate this.server.tool( "vibrate", "Vibrate the toy", { intensity: z.number().min(0).max(20).default(10).describe("Vibration strength 0-20"), duration: z.number().default(5).describe("Duration in seconds") }, async ({ intensity, duration }) => { const result = await sendCommand(this.env, { command: 'Function', action: `Vibrate:${intensity}`, timeSec: duration }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); // Vibrate pattern (pulsing) this.server.tool( "vibrate_pattern", "Vibrate with on/off pattern (pulsing)", { intensity: z.number().min(0).max(20).default(10).describe("Vibration strength 0-20"), duration: z.number().default(10).describe("Total duration in seconds"), on_sec: z.number().default(2).describe("Seconds of vibration per pulse"), off_sec: z.number().default(1).describe("Seconds of pause between pulses") }, async ({ intensity, duration, on_sec, off_sec }) => { const result = await sendCommand(this.env, { command: 'Function', action: `Vibrate:${intensity}`, timeSec: duration, loopRunningSec: on_sec, loopPauseSec: off_sec }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); // Custom pattern this.server.tool( "pattern", "Send custom intensity pattern", { strengths: z.string().default("5;10;15;20;15;10;5").describe("Semicolon-separated intensity values 0-20"), interval_ms: z.number().min(100).default(500).describe("Milliseconds between each intensity change"), duration: z.number().default(10).describe("Total duration in seconds") }, async ({ strengths, interval_ms, duration }) => { const result = await sendCommand(this.env, { command: 'Pattern', rule: `V:1;F:v;S:${interval_ms}#`, strength: strengths, timeSec: duration }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); // Preset patterns this.server.tool( "preset", "Run a built-in pattern preset: pulse, wave, fireworks, or earthquake", { name: z.enum(['pulse', 'wave', 'fireworks', 'earthquake']).default('pulse').describe("Preset name"), duration: z.number().default(10).describe("Duration in seconds") }, async ({ name, duration }) => { const result = await sendCommand(this.env, { command: 'Preset', name: name, timeSec: duration }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); // Stop this.server.tool( "stop", "Stop all toy activity immediately", {}, async () => { const result = await sendCommand(this.env, { command: 'Function', action: 'Stop', timeSec: 0 }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); // Edge this.server.tool( "edge", "Edging pattern - build up then stop, repeat", { intensity: z.number().min(0).max(20).default(15).describe("Peak vibration strength 0-20"), duration: z.number().default(30).describe("Total duration in seconds"), on_sec: z.number().default(5).describe("Seconds of vibration per cycle"), off_sec: z.number().default(3).describe("Seconds of pause between cycles") }, async ({ intensity, duration, on_sec, off_sec }) => { const result = await sendCommand(this.env, { command: 'Function', action: `Vibrate:${intensity}`, timeSec: duration, loopRunningSec: on_sec, loopPauseSec: off_sec }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); // Tease this.server.tool( "tease", "Teasing pattern - random-feeling intensity changes", { duration: z.number().default(20).describe("Duration in seconds") }, async ({ duration }) => { const pattern = '3;5;2;8;4;10;3;6;12;5;8;3;15;4;7;2;10;5'; const result = await sendCommand(this.env, { command: 'Pattern', rule: 'V:1;F:v;S:800#', strength: pattern, timeSec: duration }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); // Escalate this.server.tool( "escalate", "Gradual escalation from low to high intensity", { start: z.number().min(0).max(20).default(3).describe("Starting intensity 0-20"), peak: z.number().min(0).max(20).default(18).describe("Peak intensity 0-20"), duration: z.number().default(30).describe("Duration in seconds") }, async ({ start, peak, duration }) => { const steps = 10; const stepSize = (peak - start) / steps; const strengths: number[] = []; for (let i = 0; i <= steps; i++) { strengths.push(Math.round(start + (stepSize * i))); } const pattern = strengths.join(';'); const interval = Math.max(100, Math.floor((duration * 1000) / (steps + 1))); const result = await sendCommand(this.env, { command: 'Pattern', rule: `V:1;F:v;S:${interval}#`, strength: pattern, timeSec: duration }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); } } // Helper for QR code (used by both MCP and REST API) async function getQrCode(env: Env) { if (!env.LOVENSE_TOKEN) { return { error: 'LOVENSE_TOKEN not configured' }; } const response = await fetch(LOVENSE_QR_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: env.LOVENSE_TOKEN, uid: env.LOVENSE_UID || 'mai', uname: 'Mai', v: 2 }) }); return response.json(); } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url); const json = async () => { try { return await request.json(); } catch { return {}; } }; // Health check if (url.pathname === '/' || url.pathname === '/health') { return new Response(JSON.stringify({ status: 'ok', service: 'lovense-cloud', version: '1.0.0' }, null, 2), { headers: { 'Content-Type': 'application/json' } }); } // SSE endpoint if (url.pathname === '/sse' || url.pathname === '/sse/message') { return LovenseCloud.serveSSE('/sse', { binding: 'LOVENSE_CLOUD' }).fetch(request, env, ctx); } // MCP HTTP endpoint if (url.pathname === '/mcp') { return LovenseCloud.serve('/mcp', { binding: 'LOVENSE_CLOUD' }).fetch(request, env, ctx); } // ============ REST API ENDPOINTS (for bridge) ============ if (url.pathname === '/api/qr') { const result = await getQrCode(env); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/toys') { const result = await sendCommand(env, { command: 'GetToys' }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/vibrate') { const { intensity = 10, duration = 5 } = await json(); const result = await sendCommand(env, { command: 'Function', action: `Vibrate:${intensity}`, timeSec: duration }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/vibrate-pattern') { const { intensity = 10, duration = 10, on_sec = 2, off_sec = 1 } = await json(); const result = await sendCommand(env, { command: 'Function', action: `Vibrate:${intensity}`, timeSec: duration, loopRunningSec: on_sec, loopPauseSec: off_sec }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/pattern') { const { strengths = '5;10;15;20;15;10;5', interval_ms = 500, duration = 10 } = await json(); const result = await sendCommand(env, { command: 'Pattern', rule: `V:1;F:v;S:${interval_ms}#`, strength: strengths, timeSec: duration }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/preset') { const { name = 'pulse', duration = 10 } = await json(); const result = await sendCommand(env, { command: 'Preset', name: name, timeSec: duration }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/stop') { const result = await sendCommand(env, { command: 'Function', action: 'Stop', timeSec: 0 }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/edge') { const { intensity = 15, duration = 30, on_sec = 5, off_sec = 3 } = await json(); const result = await sendCommand(env, { command: 'Function', action: `Vibrate:${intensity}`, timeSec: duration, loopRunningSec: on_sec, loopPauseSec: off_sec }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/tease') { const { duration = 20 } = await json(); const pattern = '3;5;2;8;4;10;3;6;12;5;8;3;15;4;7;2;10;5'; const result = await sendCommand(env, { command: 'Pattern', rule: 'V:1;F:v;S:800#', strength: pattern, timeSec: duration }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/escalate') { const { start = 3, peak = 18, duration = 30 } = await json(); const steps = 10; const stepSize = (peak - start) / steps; const strengths: number[] = []; for (let i = 0; i <= steps; i++) { strengths.push(Math.round(start + (stepSize * i))); } const pattern = strengths.join(';'); const interval = Math.max(100, Math.floor((duration * 1000) / (steps + 1))); const result = await sendCommand(env, { command: 'Pattern', rule: `V:1;F:v;S:${interval}#`, strength: pattern, timeSec: duration }); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); } return new Response('Lovense Cloud MCP Server', { headers: { 'Content-Type': 'text/plain' } }); } };

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/amarisaster/Lovense-Cloud-MCP'

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