Last updated July 12, 2026
Modular ES6 JavaScript library for building multi-user web applications.
Supported from MouseMux v2.2.8+ | Current SDK: v2.2.36
The Web SDK enables real-time multi-user interaction in web applications. Each physical mouse connected to the system generates independent input events, allowing multiple users to interact simultaneously with the same application.
Key capabilities:
// 1. Create SDK instance
const sdk = new MouseMuxSDK({
appName: 'My App',
appVersion: '1.0.0'
});
// 2. Register all 10 required handlers
sdk.registerHandlers({
onPointerMotion: (data) => {
const pos = sdk.coords.screenToElement(data.x, data.y, canvas);
if (pos) drawCursor(pos.x, pos.y, data.hwid);
},
onPointerButton: (data) => {
if (data.button & MouseMuxSDK.BUTTON_LEFT_DOWN) handleClick(data);
},
onPointerWheel: () => {},
onPointerPen: () => {},
onKeyboard: () => {},
onUserList: (data) => updateUserCount(data.users.length),
onUserCreate: () => {},
onUserDispose: () => {},
onConnected: () => console.log('Connected!'),
onDisconnected: () => console.log('Disconnected')
});
// 3. Connect
sdk.connect('ws://localhost:41001');
| Requirement | Details |
|---|---|
| MouseMux V2 | Must be installed and running |
| SDK mode | Enabled in MouseMux configuration |
| Browser | Modern browser with WebSocket and ES6 support |
| Electron | Optional -- preload bridge for accurate coordinate conversion |
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My MouseMux App</title>
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div id="app-container"><!-- Your UI here --></div>
<!-- SDK Path Configuration -->
<script src="sdk-path.js"></script>
<!-- Load SDK modules in order -->
<script src="../websdk/sdk/core/events.js"></script>
<script src="../websdk/sdk/core/connection.js"></script>
<script src="../websdk/sdk/core/messages.js"></script>
<script src="../websdk/sdk/state/handlers.js"></script>
<script src="../websdk/sdk/state/devices.js"></script>
<script src="../websdk/sdk/state/users.js"></script>
<script src="../websdk/sdk/helpers/coords.js"></script>
<script src="../websdk/sdk/helpers/hit-detection.js"></script>
<script src="../websdk/sdk/helpers/cursors.js"></script>
<script src="../websdk/sdk/helpers/click-bindings.js"></script>
<script src="../websdk/sdk/input/simplified.js"></script>
<script src="../websdk/sdk/input/native.js"></script>
<script src="../websdk/sdk/index.js"></script>
<!-- Optional: UI and i18n -->
<script src="../websdk/sdk/i18n.js"></script>
<script src="../websdk/sdk/ui.js"></script>
<!-- Your app modules -->
<script src="js/app-config.js"></script>
<script src="js/app-state.js"></script>
<script src="js/app-ui.js"></script>
<script src="js/app-sdk.js"></script>
<script src="js/app-init.js"></script>
</body>
</html>
websdk/sdk/
+-- core/ # Core functionality
| +-- events.js # Event system (on/off/emit)
| +-- connection.js # WebSocket lifecycle
| +-- messages.js # Message parsing and routing
|
+-- state/ # State management
| +-- handlers.js # Handler registration
| +-- devices.js # Device state and colors
| +-- users.js # User management
|
+-- helpers/ # UI helpers
| +-- coords.js # Coordinate conversion
| +-- forwarder.js # Coordinate forwarding (screen mirror/sharing)
| +-- hit-detection.js # Hit detection wrappers
| +-- cursors.js # Remote cursor visualization
| +-- click-bindings.js # Declarative click bindings
|
+-- input/ # Input handling
| +-- simplified.js # Simplified event handlers
| +-- native.js # Debug/offline mode
|
+-- i18n.js # Internationalization
+-- ui.js # UI components
+-- index.js # Module assembler
events.js (no deps)
|
handlers.js (no deps)
|
connection.js --> events, handlers
|
devices.js --> users (optional)
|
users.js --> events, connection
|
messages.js --> events, handlers, devices, users, connection
|
coords.js (standalone)
|
forwarder.js --> coords, users (via sdk instance)
|
hit-detection.js --> coords
|
cursors.js --> events, devices, users
|
click-bindings.js --> events, hit-detection
|
simplified.js --> events, devices
|
native.js --> messages
|
index.js (assembles all)
All 10 required handlers must be registered via registerHandlers() before connecting:
| Handler | Data Fields | Description |
|---|---|---|
onPointerMotion | hwid, userId, x, y, user, deviceType, inputSource | Pointer moved |
onPointerButton | hwid, userId, x, y, button, user, deviceType, inputSource | Button pressed/released |
onPointerWheel | hwid, userId, x, y, delta, horizontal, user, deviceType, inputSource | Scroll wheel |
onPointerPen | hwid, userId, x, y, pressure, tilt_x, tilt_y, rotation, user, deviceType, inputSource | Pen/stylus input |
onKeyboard | hwid, userId, vkey, message, scan, flags, user, deviceType, inputSource | Keyboard input |
onUserList | users[] | Full user list received |
onUserCreate | hwid_ms, hwid_kb, name | New user connected |
onUserDispose | hwid_ms, hwid_kb | User disconnected |
onConnected | url | Connected to server |
onDisconnected | -- | Disconnected from server |
Input event fields:
| Field | Description |
|---|---|
hwid | Device hardware ID (different for mouse vs keyboard) |
userId | User ID -- same for mouse and keyboard from the same user. Use this to identify users. |
user | Full user object, or null if user list not requested |
deviceType | 'mouse', 'keyboard', or 'pen' |
inputSource | 'server' for MouseMux input, 'native' for browser input |
For handlers you don't need, use empty functions: onPointerWheel: () => {}
| Handler | Description |
|---|---|
onServerShutdown | Server shutting down (data: {reason}) |
onServerTimeoutWarning | Timeout warning (data: {minutes}; return true to suppress default alert) |
onServerTimeoutStopped | Session ended due to timeout (data: {reason}) |
onDeviceNew | SDK saw a new HWID for the first time (data: {hwid, data}) |
onUserMap | Keyboard mapped to existing user (data: {action, userId, hwid_ms, hwid_kb, name}) |
onUserUpdate | User state changed (data: {action, userId, ...}) |
onUserChanged | Any user state change -- create/map/dispose/update |
onScreenList | Screen configuration received (data: {virtual, screens[]}) |
onScreenChange | Screen configuration changed |
onWindowInfo | Tracked window moved/resized/changed (data: {hwnd, pid, title, ...}) |
onWindowDestroyed | Tracked window was destroyed (data: {hwnd}) |
onServerMode | Server mode changed (data: {mode[]}) |
// Async connection with error handling
try {
await sdk.connectAsync('ws://localhost:41001', { timeout: 5000 });
console.log('Connected!');
} catch (err) {
console.error('Connection failed:', err.message);
sdk.enableNativeInput(document.body);
}
The connection handshake has two parts:
1. Server Info (Server -> App)
When the WebSocket opens, the server sends server.info.notify.M2A containing server metadata and a machine fingerprint:
{
type: 'server.info.notify.M2A',
version: '3.0.1',
protocol_version: '2.2.45',
mode: 4,
buildDate: 'Mar 15 2026',
buildTime: '11:31:56',
systemId: 'A1B2C3D4-E5F60718-9A0B1C2D-3E4F5061-000000000000000000000000-000'
}
2. Client Login (App -> Server)
The SDK automatically sends a login request:
{
type: 'client.login.request.A2M',
appName: 'Your App',
appVersion: '1.0.0',
appBuildDate: '2026-03-15',
sdkVersion: '1.0.0',
sdkBuildDate: '2026-03-15'
}
Listen for the server info event:
sdk.on('server.info.notify.M2A', (data) => {
console.log('Server:', data.version, '| System:', data.systemId);
});
The server sends periodic keepalive pings. The SDK handles pong responses automatically -- no app code required.
// Try auto-connect silently, show dialog only on failure
const autoConnected = await sdk.tryAutoConnect();
if (!autoConnected) {
const result = await MouseMuxUI.showConnectionDialog({ ... });
}
// Or probe first
try {
const info = await MouseMuxSDK.probe('ws://localhost:41001');
console.log('Server running: v' + info.version);
sdk.connect('ws://localhost:41001');
} catch (err) {
console.log('Server not available');
}
// Graceful shutdown - waits for server to clean up virtual users and captures
await sdk.disconnectAsync('user');
The SDK sends button events (transitions), not cumulative state. Each button has paired DOWN and UP constants:
| Constant | Value | Event |
|---|---|---|
BUTTON_LEFT_DOWN | 0x01 | Left button pressed |
BUTTON_LEFT_UP | 0x02 | Left button released |
BUTTON_RIGHT_DOWN | 0x04 | Right button pressed |
BUTTON_RIGHT_UP | 0x08 | Right button released |
BUTTON_MIDDLE_DOWN | 0x10 | Middle button pressed |
BUTTON_MIDDLE_UP | 0x20 | Middle button released |
BUTTON_X1_DOWN | 0x40 | X1/Back pressed |
BUTTON_X1_UP | 0x80 | X1/Back released |
BUTTON_X2_DOWN | 0x100 | X2/Forward pressed |
BUTTON_X2_UP | 0x200 | X2/Forward released |
All constants are accessed via MouseMuxSDK.BUTTON_*.
const { BUTTON_LEFT_DOWN, BUTTON_LEFT_UP, BUTTON_RIGHT_DOWN } = MouseMuxSDK;
onPointerButton: (data) => {
if (data.button & BUTTON_LEFT_DOWN) handleClick(data.x, data.y, data.userId);
if (data.button & BUTTON_LEFT_UP) handleRelease(data.hwid);
if (data.button & BUTTON_RIGHT_DOWN) showContextMenu(data);
}
Since events are transitions, track cumulative state yourself:
const { BUTTON_LEFT_DOWN, BUTTON_LEFT_UP } = MouseMuxSDK;
onPointerButton: (data) => {
const device = sdk.getDevice(data.hwid);
if (data.button & BUTTON_LEFT_DOWN) device.buttonDown = true;
if (data.button & BUTTON_LEFT_UP) device.buttonDown = false;
}
MouseMux sends coordinates in physical screen pixels. The SDK provides helpers to convert to element-relative coordinates:
// Convert screen coords to element coords
const pos = sdk.coords.screenToElement(data.x, data.y, canvas);
if (pos) {
drawAt(pos.x, pos.y);
}
// Check if point is in element
if (sdk.coords.isInElement(data.x, data.y, canvas)) {
// Handle canvas interaction
}
// Find clicked element by selector
const btn = sdk.findClickedElement(data.x, data.y, '.btn');
if (btn) btn.click();
Important: Always use sdk.coords.screenToElement() -- never use raw x/y coordinates directly. The SDK handles multi-monitor layouts, DPI scaling, and Electron zoom factors.
| Path | Accuracy | How it works |
|---|---|---|
| Electron (recommended) | Accurate | Main process pushes transform cache on window move/resize. Zero IPC per event. Handles multi-monitor + mixed DPI + zoom. |
| Browser (fallback) | Approximate | Uses window.devicePixelRatio and window.screen*. Works for single monitor, same-DPI setups. |
| Range | Hex | Description |
|---|---|---|
| 0 - 4095 | 0x0000-0x0FFF | Physical hardware devices |
| 12288 - 13311 | 0x3000-0x33FF | SDK server-assigned devices |
| 13312 - 14335 | 0x3400-0x37FF | SDK client-side virtual users |
| 16383 | 0x3FFF | Native input (debug mode) |
| 24576 - 26623 | 0x6000-0x67FF | RustDesk remote devices |
const device = sdk.getDevice(hwid);
// {
// hwid: 197992480,
// buttonDown: true,
// lastX: 500, lastY: 300,
// color: '#ef4444',
// user: { name: 'John', ... },
// customData: {}
// }
// Store per-device app data
sdk.setDeviceData(hwid, 'brushColor', '#ff0000');
sdk.setDeviceData(hwid, 'brushSize', 10);
// Retrieve
const color = sdk.getDeviceData(hwid, 'brushColor');
const all = sdk.getAllDeviceData(hwid);
// Auto-assigned from palette
const color = sdk.getDeviceColor(hwid); // '#ef4444'
// Custom palette
sdk.setColorPalette([
'#3b82f6', '#ef4444', '#10b981', '#f59e0b',
'#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'
]);
const devices = sdk.getAllDevices(); // Map of all devices
const lastHwid = sdk.getLastActiveDevice(); // Most recently active HWID
const isLocal = sdk.isMyDevice(hwid); // Is this one of my devices?
const myHwids = sdk.getMyDevices(); // Set of local HWIDs
Users are physical persons with one or more devices (mouse + keyboard):
// Get user for a device
const user = sdk.getUserForDevice(hwid);
// {
// id: number,
// name: string, // Color-based name: "Red", "Blue", etc.
// flag: number,
// flags: ['active', 'root'],
// devices: [{ hwid, type, subtype, path, buttons, udev }],
// cursor: { x, y, type, size, hidden, theme },
// color: { r, g, b }
// }
// Get user ID from any device HWID (mouse or keyboard)
const userId = sdk.getUserId(hwid);
// Check if two devices belong to the same user
if (sdk.isSameUser(mouseHwid, keyboardHwid)) { /* same person */ }
// Get all HWIDs for a user
const hwids = sdk.getDevicesForUser(userId);
// Friendly device name
const name = sdk.getDeviceName(hwid); // "John's Mouse"
// Request user list from server (most apps need this)
sdk.requestUserList();
| Action | Handler | When |
|---|---|---|
create | onUserCreate | Mouse arrives, new user created |
map | onUserMap (optional) | Keyboard mapped to existing user |
dispose | onUserDispose | User removed |
All changes also trigger onUserChanged(data) where data.action indicates the change type.
| Value | Description |
|---|---|
mouse | Standard mouse |
pen_external | External pen/stylus |
pen_internal | Built-in pen (tablet) |
touch | Touch input |
touchpad | Touchpad |
| Flag | Description |
|---|---|
active | User is active |
root | Admin user |
locked | User locked |
captured | Device captured (SDK) |
cursor_hidden | Cursor hidden |
cursor_move_locked | Movement locked |
cursor_type_locked | Type locked |
macro_recording | Recording macro |
macro_playing | Playing macro |
Create programmatic users without a server connection. Each virtual user gets a deterministic cursor color and name tag.
sdk.enableVirtualInput();
// Create virtual users
const alice = sdk.createVirtualUser('Alice');
// { hwid_ms: 0x3400, hwid_kb: 0x3401, name: 'Alice', color: '#e6194b' }
const bob = sdk.createVirtualUser('Bob');
// { hwid_ms: 0x3402, hwid_kb: 0x3403, name: 'Bob', color: '#3cb44b' }
// Inject input events
sdk.injectVirtualPointerMotion(alice.hwid_ms, 500, 300);
sdk.injectVirtualPointerButton(alice.hwid_ms, 500, 300, MouseMuxSDK.BUTTON_LEFT_DOWN);
sdk.injectVirtualPointerWheel(bob.hwid_ms, 200, 100, 120);
sdk.injectVirtualKeyboard(alice.hwid_kb, 0x41, 0x100); // 'A' key down
// Convenience: instant down+up click
sdk.injectVirtualClick(alice.hwid_ms, 500, 300);
// Update name
sdk.setVirtualUserName(alice.hwid_ms, 'Alice (host)');
// Cleanup
sdk.destroyVirtualUser(alice.hwid_ms);
sdk.disableVirtualInput(); // destroys all remaining
Virtual users integrate with remote cursors automatically:
sdk.enableRemoteCursors(document.body, { showLabels: true });
sdk.enableVirtualInput();
const vu = sdk.createVirtualUser('Remote Viewer');
sdk.injectVirtualPointerMotion(vu.hwid_ms, 400, 200); // cursor appears
Colors are deterministic by HWID slot -- destroying and recreating at the same slot gives the same color.
For screen sharing, remote desktop, and mirroring apps, the SDK's CoordinateForwarder maps coordinates between a video element showing a captured screen and the target screen.
| Mode | How it works | When to use |
|---|---|---|
| Manual | Client-side JS: screenToElement -> normalize -> de-normalize to target screen | Full client-side control |
| Server | Raw physical coords + transform ID sent to server; server does the mapping | Production use |
Both modes use (w-1) normalization to match the server's integer math exactly.
// 1. Connect and get screen list
await sdk.connectAsync('ws://localhost:41001');
sdk.requestScreenList();
// 2. Create forwarder
const fwd = sdk.createCoordinateForwarder(videoElement, screenIndex, {
mode: 'server'
});
await fwd.activate();
// 3. Forward coordinates in your handler
onPointerMotion: (data) => {
const params = fwd.buildMotionParams(vuMouseHwid, data.x, data.y);
if (params) sdk.send('pointer.motion.request.A2M', params);
}
// 4. Cleanup
fwd.destroy();
| Property | Type | Description |
|---|---|---|
mode | 'manual' or 'server' | Forwarding mode |
active | boolean | Whether currently active |
transformId | number or null | Server-side transform ID (server mode) |
targetScreen | object or null | Target screen from screen list |
element | HTMLElement | Source DOM element |
screenIndex | number | Target screen index |
const info = await sdk.requestWindowInfo(hwnd);
// {
// hwnd: 12345, pid: 9876,
// title: 'Untitled - Notepad',
// monitor: 0, dpi: 144,
// state: 'normal', visible: true,
// window: { x: 100, y: 200, w: 800, h: 600 },
// client: { x: 108, y: 230, w: 784, h: 561 }
// }
const info = await sdk.trackWindow(hwnd);
sdk.registerHandlers({
// ... required handlers ...
onWindowInfo: (data) => {
console.log('Window changed:', data.title, data.state);
},
onWindowDestroyed: (data) => {
console.log('Window destroyed:', data.hwnd);
}
});
sdk.untrackWindow(hwnd); // stop tracking
// Convenience methods
await sdk.showWindow(hwnd);
await sdk.hideWindow(hwnd);
await sdk.minimizeWindow(hwnd);
await sdk.maximizeWindow(hwnd);
await sdk.restoreWindow(hwnd);
await sdk.foregroundWindow(hwnd);
await sdk.focusWindow(hwnd);
await sdk.topWindow(hwnd);
await sdk.enableWindow(hwnd);
await sdk.disableWindow(hwnd);
await sdk.closeWindow(hwnd);
await sdk.moveWindow(hwnd, 100, 200);
await sdk.resizeWindow(hwnd, 800, 600);
await sdk.setWindowTransparent(hwnd, 128); // 50% opacity
await sdk.setWindowTopmost(hwnd);
await sdk.clearWindowTopmost(hwnd);
await sdk.setWindowClickthrough(hwnd);
await sdk.clearWindowClickthrough(hwnd);
// Or use the generic method
await sdk.controlWindow(hwnd, 'minimize');
await sdk.controlWindow(hwnd, 'move', { x: 100, y: 200 });
await sdk.controlWindow(hwnd, 'resize', { w: 800, h: 600 });
await sdk.controlWindow(hwnd, 'transparent', { alpha: 128 });
| Action | Description | Params |
|---|---|---|
show | Show window | -- |
hide | Hide window | -- |
minimize | Minimize to taskbar | -- |
maximize | Maximize window | -- |
restore | Restore from min/max | -- |
foreground | Bring to foreground | -- |
focus | Set keyboard focus | -- |
top | Bring to top of Z-order | -- |
enable | Enable mouse/keyboard input | -- |
disable | Disable mouse/keyboard input | -- |
close | Close window | -- |
move | Move to position | x, y |
resize | Resize window | w, h |
transparent | Set opacity | alpha (0-255) |
topmost | Always on top | -- |
no_topmost | Remove always on top | -- |
clickthrough | Make click-through | -- |
no_clickthrough | Restore mouse input | -- |
The same handlers work for both MouseMux server input and native browser input:
sdk.registerHandlers({
onPointerMotion: (data) => {
// Works for BOTH server and native input
// data.inputSource: 'server' or 'native'
const pos = sdk.coords.screenToElement(data.x, data.y, canvas);
if (pos) handleMotion(pos.x, pos.y, data.hwid);
},
onPointerButton: (data) => {
if (data.button & MouseMuxSDK.BUTTON_LEFT_DOWN) handleClick(data);
}
// ...
});
// Connect to server OR enable native input - handlers work either way
if (result.action === 'connect') {
sdk.connect(result.url);
} else {
sdk.enableNativeInput(document.body);
}
Rules:
addEventListener callsuseMouseMux flags in handlersdata.inputSource if you need to distinguish sourcessdk.bindClick() for UI buttons (works for both modes)onPointerMotion: (data) => {
const pos = sdk.coords.screenToElement(data.x, data.y, canvas);
if (pos) {
// pos.x, pos.y are element-relative
}
}
onPointerPen: (data) => {
const pressure = data.pressure / 1023; // normalize to 0-1
const minSize = 2, maxSize = 20;
const size = minSize + (maxSize - minSize) * pressure;
}
onKeyboard: (data) => {
const isDown = data.message === 0x100; // WM_KEYDOWN
const isUp = data.message === 0x101; // WM_KEYUP
if (data.vkey === 0x42 && isDown) setTool('brush'); // 'B'
if (data.vkey === 0x45 && isDown) setTool('eraser'); // 'E'
}
Declaratively bind clicks to elements:
sdk.bindClick('.color-swatch', (element, hwid) => {
const color = element.dataset.color;
sdk.setDeviceData(hwid, 'selectedColor', color);
element.style.borderColor = sdk.getDeviceColor(hwid);
});
sdk.bindClick('.answer-card', (element, hwid) => {
state.votes.set(hwid, parseInt(element.dataset.index));
element.style.boxShadow = '0 0 10px ' + sdk.getDeviceColor(hwid);
updateVoteDisplay();
});
sdk.clearClickBindings(); // remove all
sdk.enableRemoteCursors(document.body, {
showLabels: true,
autoHide: false,
customHTML: (hwid, device) => {
return '<div style="color:' + device.color + ';">●</div>';
}
});
sdk.disableRemoteCursors();
const result = await MouseMuxUI.showConnectionDialog({
appName: 'My App',
appDescription: 'Multi-user collaborative application',
defaultUrl: 'ws://localhost:41001',
showThemeOption: true,
showLanguageOption: true,
onThemeChange: (theme) => applyTheme(theme),
onLanguageChange: (lang) => updateStrings()
});
if (result.action === 'connect') {
sdk.connect(result.url);
} else if (result.action === 'skip') {
sdk.enableNativeInput(document.body);
}
MouseMuxUI.showBanner({
appName: 'My App',
appVersion: '1.0.0',
appDate: '2026-03-15',
appTime: '11:31:56',
sdk: sdk,
showThemeToggle: true,
showLanguageSelector: true
});
MouseMuxUI.toast('Connected!', { type: 'success' });
MouseMuxUI.toast('Warning message', { type: 'warning' });
MouseMuxUI.toast('Error occurred', { type: 'error', duration: 5000 });
MouseMuxUI.toast('Info', { type: 'info' });
const confirmed = await ui.confirm('Are you sure?', {
title: 'Confirm Action',
confirmText: 'Yes',
cancelText: 'No'
});
const danger = await ui.confirmDanger('Delete all data?', {
title: 'Danger',
confirmText: 'Delete'
});
9 languages supported: English, German, Spanish, French, Italian, Japanese, Korean, Dutch, Chinese.
// SDK locales only
await MouseMuxI18n.init({
localesPath: '../websdk/locales',
defaultLanguage: 'en'
});
// SDK + app locales
await MouseMuxI18n.init({
localesPath: '../websdk/locales',
appLocales: {
path: './locales',
namespace: 'myapp'
}
});
MouseMuxI18n.t('sdk.connected'); // SDK string
MouseMuxI18n.t('myapp.brushSize'); // App string
MouseMuxI18n.t('sdk.timeoutWarning', { minutes: 5 }); // With params
await MouseMuxI18n.setLanguage('de');
const lang = MouseMuxI18n.getLanguage();
MouseMuxI18n.getInstance().on('languageChanged', (lang) => {
updateUIStrings();
});
{
"_meta": { "language": "English", "code": "en" },
"appName": "My App",
"brushSize": "Brush Size",
"clearCanvas": "Clear Canvas",
"greeting": "Hello, {{name}}!"
}
Test your app without a MouseMux server:
try {
await sdk.connectAsync('ws://localhost:41001', { timeout: 2000 });
} catch (err) {
console.log('Server unavailable, using native input');
sdk.enableNativeInput(document.body);
}
if (sdk.isNativeInputEnabled()) {
console.log('Running in debug mode');
}
sdk.disableNativeInput();
Native input uses HWID 0x3FFF for the single debug device.
The server sends a systemId string on connect -- a machine fingerprint that uniquely identifies the host system.
{CPUID}-{MAC}-{MACHINEGUID}-{SID}-{LICENSE}
Four 8-character uppercase hex fields (zero-padded), followed by a variable-length license string.
Example: A1B2C3D4-E5F60718-9A0B1C2D-3E4F5061-000000000000000000000000-000
const sysId = sdk.getSystemId();
if (sdk.serverInfo) {
console.log('System ID:', sdk.serverInfo.systemId);
console.log('Version:', sdk.serverInfo.version);
}
sdk.on('server.info.notify.M2A', (data) => {
console.log('System ID:', data.systemId);
});
function parseSystemId(systemId) {
const parts = systemId.split('-');
return {
cpuid: parts[0],
mac: parts[1],
machineGuid: parts[2],
sid: parts[3],
license: parts.slice(4).join('-')
};
}
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('mmxElectron', {
getTransform: () => ipcRenderer.invoke('mmx-get-transform'),
onTransformUpdate: (cb) => {
const handler = (_, t) => cb(t);
ipcRenderer.on('mmx-transform-update', handler);
return () => ipcRenderer.removeListener('mmx-transform-update', handler);
},
onShutdown: (cb) => {
const handler = async () => {
await cb();
ipcRenderer.send('mmx-shutdown-complete');
};
ipcRenderer.on('mmx-shutdown', handler);
return () => ipcRenderer.removeListener('mmx-shutdown', handler);
}
});
Important: The onShutdown handler ensures the SDK can send client.logout.request.A2M and wait for the response before the window closes. Without this, virtual users and captures may be orphaned.
const { screen, ipcMain } = require('electron');
function pushTransform() {
const transform = {
zoomFactor: win.webContents.getZoomFactor(),
contentBoundsDip: win.getContentBounds(),
displays: screen.getAllDisplays().map(d => ({
id: d.id,
scaleFactor: d.scaleFactor,
boundsDip: d.bounds
}))
};
win.webContents.send('mmx-transform-update', transform);
}
const debounce = (fn, ms) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), ms);
};
};
win.on('resize', debounce(pushTransform, 50));
win.on('move', debounce(pushTransform, 50));
ipcMain.handle('mmx-get-transform', pushTransform);
// Graceful shutdown
let shutdownComplete = false;
ipcMain.on('mmx-shutdown-complete', () => {
shutdownComplete = true;
win.destroy();
});
win.on('close', (e) => {
if (!shutdownComplete) {
e.preventDefault();
win.webContents.send('mmx-shutdown');
setTimeout(() => { shutdownComplete = true; win.destroy(); }, 2000);
}
});
myapp/
+-- index.html # Entry point
+-- sdk-path.js # SDK path configuration
+-- app.json # App metadata
+-- css/
| +-- app.css # Styles
+-- js/
| +-- app-config.js # Constants and configuration
| +-- app-state.js # State management
| +-- app-ui.js # UI helpers and rendering
| +-- app-sdk.js # SDK integration
| +-- app-init.js # Initialization
+-- locales/
+-- en.json # English strings
+-- de.json # German strings
Apps must follow this exact order:
(async function initializeApp() {
// 1. Load app metadata
let appInfo = { name: 'My App', info: { make: '2.2.36' } };
try {
const response = await fetch('./app.json');
if (response.ok) appInfo = await response.json();
} catch (e) { console.warn('Could not load app.json'); }
// 2. Create SDK instance
const sdk = new MouseMuxSDK({
appName: appInfo.name,
appVersion: appInfo.info?.make || '2.2.36',
autoConnect: false
});
// 3. Register handlers (before connection dialog!)
sdk.registerHandlers({ /* all 10 handlers */ });
// 4. Initialize i18n
await MouseMuxI18n.init({
localesPath: MOUSEMUX_SDK_PATH + 'locales',
appLocales: { path: './locales', namespace: 'myapp' }
});
// 5. Show connection dialog
const result = await MouseMuxUI.showConnectionDialog({
appName: appInfo.name,
showThemeOption: true,
showLanguageOption: true
});
// 6. Connect or enable native input
if (result.action === 'connect') {
sdk.connect(result.url);
} else {
sdk.enableNativeInput(document.body);
}
// 7. Show status banner
MouseMuxUI.showBanner({
appName: appInfo.name,
appVersion: appInfo.info?.make,
appDate: appInfo.date,
appTime: appInfo.time,
sdk: sdk,
showThemeToggle: true
});
})();
Declare which operating modes your app needs:
{
"mode": {
"hard": ["multiplex"],
"hint": ["switched"],
"help": "Works best in Switched mode for independent cursors"
}
}
| Field | Type | Description |
|---|---|---|
hard | string[] | Required modes -- app will not function without these |
hint | string[] | Suggested modes -- app works best with these |
help | string | Human-readable explanation |
Mode values: "native", "switched", "multiplex", "multi-keyboard"
| Option | Type | Default | Description |
|---|---|---|---|
appName | string | 'Unknown App' | Application name (sent to server on login) |
appVersion | string | '0.0.0' | Application version |
appBuildDate | string | 'unknown' | Application build date |
url | string | 'ws://localhost:41001' | WebSocket server URL |
autoConnect | boolean | true | Auto-connect on construction |
strictValidation | boolean | false | Enable strict field validation on messages |
| Method | Returns | Description |
|---|---|---|
connect(url?) | void | Connect to WebSocket server |
connectAsync(url?, options?) | Promise | Connect with Promise and timeout |
tryAutoConnect() | Promise<boolean> | Try auto-connect silently |
disconnect() | void | Disconnect from server |
disconnectAsync(reason?) | Promise | Graceful disconnect with logout |
isConnected() | boolean | Check connection status |
send(type, params?) | boolean | Send message to server |
sendLog(level, message) | boolean | Send log message to server |
| Method | Returns | Description |
|---|---|---|
getDevice(hwid) | object | Get device state |
getAllDevices() | Map | Get all devices |
getLastActiveDevice() | number | Get last active HWID |
isMyDevice(hwid) | boolean | Check if local device |
getMyDevices() | Set | Get all local HWIDs |
setDeviceData(hwid, key, value) | void | Store custom data |
getDeviceData(hwid, key) | any | Get custom data |
getAllDeviceData(hwid) | object | Get all custom data |
getDeviceColor(hwid) | string | Get auto-assigned color |
setColorPalette(colors) | void | Set custom color palette |
getColorPalette() | string[] | Get current palette |
| Method | Returns | Description |
|---|---|---|
requestUserList() | boolean | Request user list from server |
getUserId(hwid) | number | Get user ID for any device |
isSameUser(hwid1, hwid2) | boolean | Check if same user |
getDevicesForUser(userId) | number[] | Get all HWIDs for a user |
getUserForDevice(hwid) | object | Get full user object |
getDeviceName(hwid) | string | Get friendly device name |
getUsers() | Map | Get all users |
getStats() | object | Get event statistics |
getSystemId() | string | Get system ID of connected server |
getServerInfo(options?) | Promise | Get server info |
requestServerInfo() | void | Request server info |
requestServerPath(what, options?) | Promise | Resolve path key to filesystem path |
requestHardwareIDs(options?) | Promise | Request virtual HWIDs |
requestServerMode() | void | Request current server mode |
| Method | Returns | Description |
|---|---|---|
requestScreenList() | boolean | Request monitor/screen list |
activateTransform(params) | boolean | Activate coordinate transform |
updateTransform(params) | boolean | Update transform viewport |
deactivateTransform(params) | boolean | Deactivate transform |
createCoordinateForwarder(el, screenIdx, opts?) | CoordinateForwarder | Create coordinate forwarder |
| Method | Returns | Description |
|---|---|---|
requestWindowInfo(hwnd, options?) | Promise<Object> | One-shot window info query |
trackWindow(hwnd, options?) | Promise<Object> | Subscribe to live window updates |
untrackWindow(hwnd) | boolean | Unsubscribe from window updates |
controlWindow(hwnd, action, params?, options?) | Promise<Object> | Control a window |
showWindow(hwnd) | Promise | Show window |
hideWindow(hwnd) | Promise | Hide window |
minimizeWindow(hwnd) | Promise | Minimize window |
maximizeWindow(hwnd) | Promise | Maximize window |
restoreWindow(hwnd) | Promise | Restore window |
foregroundWindow(hwnd) | Promise | Bring to foreground |
focusWindow(hwnd) | Promise | Set keyboard focus |
topWindow(hwnd) | Promise | Bring to top |
closeWindow(hwnd) | Promise | Close window |
moveWindow(hwnd, x, y) | Promise | Move window |
resizeWindow(hwnd, w, h) | Promise | Resize window |
setWindowTransparent(hwnd, alpha) | Promise | Set opacity (0-255) |
setWindowTopmost(hwnd) | Promise | Always on top |
clearWindowTopmost(hwnd) | Promise | Remove always on top |
setWindowClickthrough(hwnd) | Promise | Make click-through |
clearWindowClickthrough(hwnd) | Promise | Restore mouse input |
sdk.coords)| Method | Returns | Description |
|---|---|---|
screenToElement(x, y, el) | {x,y} or null | Convert to element coords |
screenToContentCss(x, y) | {x,y} or null | Convert to CSS coords |
isInElement(x, y, el) | boolean | Check if point is in element |
elementAtPoint(x, y) | Element or null | Get element at screen coords |
hasTransformCache() | boolean | Check conversion accuracy |
hasElectronBridge() | boolean | Check if Electron bridge available |
onTransformUpdate(cb) | function | Subscribe to transform updates |
getElementPhysicalBounds(el) | {x,y,w,h} or null | Get element bounds in physical pixels |
findClickedElement(x, y, selector) | Element or null | Find element at coords |
| Method | Returns | Description |
|---|---|---|
bindClick(selector, callback) | function | Bind click to selector |
clearClickBindings() | void | Remove all bindings |
enableNativeInput(container) | void | Enable native input mode |
disableNativeInput() | void | Disable native input mode |
isNativeInputEnabled() | boolean | Check if native input active |
| Method | Returns | Description |
|---|---|---|
enableVirtualInput() | void | Enable virtual input mode |
disableVirtualInput() | void | Disable and destroy all virtual users |
isVirtualInputEnabled() | boolean | Check if enabled |
createVirtualUser(name?) | {hwid_ms, hwid_kb, name, color} | Create virtual user |
destroyVirtualUser(hwid_ms) | void | Destroy a virtual user |
getVirtualUsers() | Map | Get all active virtual users |
setVirtualUserName(hwid_ms, name) | void | Update display name |
injectVirtualPointerMotion(hwid, x, y) | void | Inject pointer motion |
injectVirtualPointerButton(hwid, x, y, button) | void | Inject button event |
injectVirtualPointerWheel(hwid, x, y, delta) | void | Inject wheel event |
injectVirtualKeyboard(hwid, vkey, message, scan?, flags?) | void | Inject keyboard event |
injectVirtualClick(hwid, x, y, domButton?) | void | Instant down+up click |
| Method | Returns | Description |
|---|---|---|
enableRemoteCursors(container, opts) | void | Show remote cursors |
disableRemoteCursors() | void | Hide remote cursors |
| Method | Returns | Description |
|---|---|---|
on(event, callback) | void | Register event listener |
off(event, callback) | void | Remove event listener |
emit(event, data) | void | Emit event |
| Method | Returns | Description |
|---|---|---|
MouseMuxSDK.probe(url, options?) | Promise | Test server availability |
*.request.A2M -- App to MouseMux (outgoing commands)*.notify.M2A -- MouseMux to App (incoming events)All A2M commands return a response with ok: true/false and optional error string.
| Message | Description |
|---|---|
server.info.notify.M2A | Server info (version, systemId, protocol_version) |
server.mode.notify.M2A | Server mode (array: ["multiplex", "multi-keyboard"]) |
server.ping.notify.M2A | Keepalive ping (SDK auto-responds) |
server.shutdown.notify.M2A | Server shutting down |
server.timeout.warning.notify.M2A | Timeout warning |
server.timeout.stopped.notify.M2A | Session ended due to timeout |
screen.list.notify.M2A | Screen/monitor configuration |
screen.change.notify.M2A | Screen config changed |
user.list.notify.M2A | Full user list |
user.create.notify.M2A | User created |
user.dispose.notify.M2A | User removed |
user.changed.notify.M2A | User state changed |
pointer.motion.notify.M2A | Pointer moved |
pointer.button.notify.M2A | Button changed |
pointer.wheel.notify.M2A | Wheel scroll |
pointer.pen.notify.M2A | Pen input |
keyboard.key.notify.M2A | Key event |
window.info.notify.M2A | Tracked window changed |
window.destroyed.notify.M2A | Tracked window destroyed |
| Message | Description |
|---|---|
client.login.request.A2M | Login to server (automatic) |
client.logout.request.A2M | Logout from server |
client.pong.request.A2M | Pong response (automatic) |
client.log.request.A2M | Send log message |
user.create.request.A2M | Create virtual user |
user.dispose.request.A2M | Remove virtual user |
user.list.request.A2M | Request user list |
user.hwid.request.A2M | Request available HWIDs |
pointer.motion.request.A2M | Move pointer |
pointer.button.request.A2M | Send button event |
pointer.wheel.request.A2M | Send wheel event |
pointer.capture.request.A2M | Capture mouse |
keyboard.key.request.A2M | Send keyboard event |
cursor.hide.request.A2M | Show/hide cursor |
cursor.type.request.A2M | Set cursor type |
screen.list.request.A2M | Request screen list |
transform.activate.request.A2M | Activate coordinate transform |
window.info.request.A2M | Query window info |
window.track.request.A2M | Subscribe to window changes |
window.control.request.A2M | Control a window |
server.info.request.A2M | Request server info |
server.mode.request.A2M | Request server mode |
server.path.request.A2M | Resolve path key |
theme.list.request.A2M | Request cursor theme list |
The Web SDK is included with every MouseMux app. Download any app from our App Library to get started -- each app contains a complete, working websdk/ folder structure.
What's included:
sdk/core/, sdk/state/, sdk/helpers/, sdk/input/)sdk/ui.js)locales/)See these apps in our App Library for working examples:
| App | Demonstrates |
|---|---|
| Multi-Paint | Collaborative drawing with brush/eraser tools |
| Team Vote | Multi-user voting and quiz system |
| Whiteboard | Shared whiteboard with shapes and text |
| SCADA Simulator | Industrial control panel demonstration |
| Service Desk | Multi-user customer support interface |
| Sound Board | Collaborative music application |
Each app demonstrates different SDK features and patterns. Download any app to explore its source code.
| Problem | Solution |
|---|---|
| Cannot connect | Verify MouseMux is running on ws://localhost:41001 |
| Connection drops | Check network stability, enable reconnect |
| Wrong URL | Check WebSocket URL in connection dialog |
| Problem | Solution |
|---|---|
| Coordinates wrong | Check sdk.coords.hasTransformCache() |
| Multi-monitor problems | Ensure displays array is populated |
| Electron zoom issues | Verify preload bridge is set up |
| Problem | Solution |
|---|---|
| Handlers not firing | Call registerHandlers() before connect() |
| Missing handlers error | Provide all 10 required handlers |
| Handler throws error | Add try-catch in handler code |
For more information or custom integration support, contact [email protected]