feat(shortcut-remote): JIT-discovered MCPAQL adapter + sidecar generalization#28
Open
mickdarling wants to merge 12 commits into
Open
feat(shortcut-remote): JIT-discovered MCPAQL adapter + sidecar generalization#28mickdarling wants to merge 12 commits into
mickdarling wants to merge 12 commits into
Conversation
First example demonstrating just-in-time observational HID discovery:
a complete CRUDE adapter for an undocumented vendor-locked keypad,
built end-to-end with no vendor SDK, kernel extension, or published
protocol.
The 12-byte vendor-page (0xFF0A) report layout was derived purely from
observed input reports while a human pressed each button. K# to bit
mapping verified live. XP-Pen Layer I-IV mappings decoded from the
~/.xppen/config.xml ACK05 section (the active device tag — the
ShortcutRemote tag in the same file is a stale legacy template).
Layout:
- adapter/ MCP server (stdio) + HTTP/WebSocket HUD on 127.0.0.1:47832
- sidecar/ Standalone keystroke-synthesis daemon listening to the
HUD WebSocket; default passthrough K7 (SuperWhisper)
and K8 (Escape), all other buttons LLM-routed
- tools/ capture-hid.js (observational discovery) and
parse-xppen-config.js (decodes config.xml mappings)
- capture/ 235 raw HID reports + DiscoveryBundle
- validation/ End-to-end smoke test + single-press listener
- README.md Architecture, decode method, install steps,
vision threads (smart-paste, app-context-aware,
multi-device fleet)
The HUD HTML hot-reloads per HTTP request so future visualization
changes land on browser refresh without restarting the MCP server.
node-hid uses kIOHIDOptionsTypeSeizeDevice on macOS, so the adapter
filters to vendor-defined pages only (usagePage >= 0xFF00) to avoid
hijacking the OS keyboard interface; release_device hands the device
back to the XP-Pen daemon mid-session.
Demonstrates the three-layer pattern: hardware → MCP-AQL adapter
(observe contract) → sidecar (act contract) → OS or LLM (dynamic
meaning). Generalizes to Stream Decks, MIDI controllers, drawing
tablets, gamepads — any HID input device becomes a context-aware
programmable surface via the same shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-file durable record of every fact derived during JIT discovery and config parsing. Source of truth for the device protocol decode; parser code and decoded JSON outputs are mechanical derivations. Covers: - Device USB identification (vendor/product IDs, internal model code ACK05) - HID interface layout and why we restrict opens to vendor pages - 12-byte vendor-page report layout for both button/wheel and battery heartbeat report families - K1-K11 to HID bit mapping (verified live) - Physical layout diagram matching XP-Pen UI graphic at rotation 90 - ~/.xppen/config.xml location and structure (ACK05 active section vs ShortcutRemote stale legacy template) - Layer encoding (Layer I-IV via K, K_1, K_2, K_3 elements) - Per-key XML entry format (default vs customized) - Pipe-separated payload format breakdown - Qt key constants and macOS virtual keycode reference table - Layer I config snapshot - The Actid limitation and two recovery paths (force-customize vs empirical capture) - Discovery method walkthrough If only this file survives, the parser, schema, and adapter can be rebuilt from it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…v var + HANDOFF
Three improvements to the keypad project, none coupled to anything else:
## Sidecar PASSTHROUGH supports shell commands
The `PASSTHROUGH` table now accepts two action shapes:
1. Keystroke synthesis (existing): `{ mac_code, modifiers, label }`
2. Shell command (new): `{ command, args, label }`
Each entry fires on the press edge of the configured button. The keystroke
shape goes through `osascript` (needs Accessibility); the command shape just
spawns the binary detached. Lets users wire any K-button to any local
side-effect — toggling apps, sending signals to other sidecars, running
shell scripts, etc. — without touching code beyond their own config.
## Default PASSTHROUGH is empty
Previously the K7 / K8 hardcoded entries were double-firing: the device's
firmware sends `Opt+Cmd+3` / `Escape` via interface 0 (the keyboard
interface) regardless of who holds interface 2 (the vendor-page interface
this adapter watches), and our `osascript` synthesis was firing the same
keystrokes a second time. Defaulting to empty avoids that double-fire and
keeps the sidecar generic — users add only the entries they want, knowing
firmware already handles its own bindings.
The header comment in `sidecar/index.js` documents both action shapes with
examples.
## SHORTCUT_REMOTE_AUTO_OPEN env var
When `SHORTCUT_REMOTE_AUTO_OPEN=1` is set, the adapter opens the HID device
at startup instead of waiting for the first MCP read call. Useful when
running the sidecar standalone (no MCP host driving the adapter) — the
sidecar starts receiving HUD WebSocket events immediately. Default
behavior unchanged when the var is unset; on-demand open as before.
## HANDOFF.md added
The session-continuity doc that was generated for the original keypad
build but never committed. Mirrors the structure of other generator-style
docs in this repo: mission, deployed surfaces, repo map, what works, what's
open, pitfalls/lessons, how to bring up a fresh session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These five files were created during the 2026-04-28 session — the kill-switch CLI script (`kill-shortcut-remote-mcp.sh`), its menu-bar Swift wrapper (`ShortcutRemoteKillSwitch.swift`), and the build / install / uninstall scripts that turn the .swift into an .app and load its launch agent. They've been on disk and used since 2026-04-28 but were never committed. Checking them in here before the next commits retire them functionally in favour of the HUD's Server panel, so the historical record is intact and a future session can either build on them or delete them deliberately rather than discovering them as orphan untracked files. The CLI script (`kill-shortcut-remote-mcp.sh`) survives as the terminal escape hatch for "stop the adapter when the HUD is unreachable" — documented in the README's terminal-escape-hatches section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… into adapter Sidecar process retired on 2026-05-02; keystroke synthesis, shell dispatch, and per-layer key lookup now live in the adapter itself (see the next commit). This file is kept for git-history reference but is no longer wired — its launchd plist was bootouted and renamed `~/Library/LaunchAgents/org.mcpaql.shortcut-remote-sidecar.plist.retired-2026-05-02`, and nothing on the system runs `sidecar/index.js` anymore. The diff here just refreshes the source so it's coherent at the moment it became dead code: the sidecar's PASSTHROUGH table records the K7 → SuperWhisper / K8 → Escape muscle-memory bindings as they were at retirement (those moved into the adapter's HARDWIRED_ACTIONS map), and the rest of the file's layer-aware lookup logic is the same shape that got moved into the adapter's `actionForKeyAtLayer`. Why retire: the sidecar fan-out only made sense when the adapter was stdio-spawned per Claude Code session and couldn't synthesize anything itself. Once the adapter became a long-lived launchd-managed daemon holding the HID handle as a singleton, the sidecar was just a second Node process subscribed to its own host's WebSocket — pure fan-out with no independent state. Folding it into the adapter gives one process, one log, one launchd job, one Accessibility-permission grant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The adapter is now a single long-lived launchd-managed process that
does five things from one port (127.0.0.1:47832): HID observer, MCP-AQL
endpoint (streamable HTTP, stateful per-session transports), HUD HTTP
+ WebSocket server, keystroke / shell / AppleScript dispatch (formerly
the sidecar), and SuperWhisper recordings-folder watcher (replaces the
per-mode `script` field SuperWhisper 2.13 dropped from its UI).
Why: simpler to reason about (one process, one log, one launchd job),
eliminates the sidecar/menu-bar fan-out, and makes the adapter's value
proposition concrete — "an MCP-AQL adapter you install and have a
working keypad" rather than three coordinated processes the user has
to remember to start. This is also the project's first real
demonstration of the philosophy that MCP-AQL adapters are capable
opinionated layers in their own right, not minimal pipes between
upstream and an MCP client.
## What landed
### Streamable HTTP MCP transport with per-session servers
stdio → streamable HTTP. Multiple Claude Code sessions can connect
concurrently; each gets its own `Server` instance and session ID, but
the underlying HID device + layer + wheel state are process-wide
singletons fanned out across all of them.
### Adapter as authoritative state holder
* `state.currentLayer` (1-4, advanced by K2; broadcast as `layer_change`)
* `state.wheelValue` (signed integer counter; broadcast as `wheel_state`)
* `state.wheelBinding` (`null` = AI mode; broadcast as `wheel_binding_change`)
* `state.lastAnnotation` (LLM-postable HUD overlay payload; broadcast as `annotation`)
Snapshots of all four go to every newly-connected WebSocket client so
HUDs come up in sync without having to wait for the next event.
### New CRUDE operations (registered in schema.json)
READ: get_current_layer, get_wheel_value, get_wheel_binding
UPDATE: set_current_layer, set_wheel_value, reset_wheel_value,
set_wheel_binding, post_annotation, submit_voice_command
`set_wheel_binding` accepts three action shapes: `{mac_code, modifiers?,
label?}` for keystrokes, `{applescript, label?}` for raw AppleScript
(needed for system actions like volume / brightness that aren't
keystrokes), `{command, args?, label?}` for shell exec.
### Hardwired keypad bindings (consistent across all layers)
| Key | Action |
|-----|--------|
| K1 | Open / focus HUD (uses `hud_focus` WS broadcast if a tab is connected, else `open URL`) |
| K2 | Advance layer 1→2→3→4→1 |
| K7 | SuperWhisper default mode (Opt+Cmd+3) |
| K8 | Escape |
| K11 | Voice-command trigger (calls `tools/k11-voice-trigger.sh` — see later commit) |
K3-K6, K9, K10 dispatch per-layer keystrokes from `xppen-mappings.json`.
K11 + the wheel together are the AI input surface (no synthesis on the
wheel by default; `set_wheel_binding` flips that).
### HUD-driven server lifecycle (replaces the menu-bar Kill Switch app)
* `POST /control/restart` — `launchctl kickstart -k`
* `POST /control/stop` — clean exit (launchd does not auto-restart;
RunAtLoad still applies at next login)
* `POST /control/disable` — `launchctl disable` + `launchctl bootout`
(won't auto-restart at login until the user
re-enables from terminal — the response
body includes the exact re-enable command)
* `GET /control/status` — PID, uptime, current layer, wheel value,
open MCP session count, device-open flag
### Voice command pipeline
`POST /voice-command` HTTP endpoint and `submit_voice_command` MCP op
both feed into `triggerVoiceCommand()`, which spawns
claude -p <system_prompt + transcribed text>
--allowedTools "mcp__shortcut-remote__mcpaql_read
mcp__shortcut-remote__mcpaql_update"
The allowlist scopes Claude to just the keypad-config tools — every
other tool still goes through the user's normal hook/permission system.
**Argv ordering matters**: prompt must come BEFORE `--allowedTools`
because the latter is variadic and would otherwise eat the prompt as
tool names. Comment in the code explains.
System prompt teaches Claude when to use which action shape (keystroke
vs AppleScript vs shell) with recipes for common system actions.
### SuperWhisper recordings-folder watcher
`startSuperWhisperWatcher()` watches `~/Documents/superwhisper/recordings/`
non-recursively (avoids cost of recursive-watching the user's massive
recordings backlog). On each new subdirectory it sets up a brief
secondary watch for `meta.json`, then calls `processSuperWhisperMeta()`.
The processor filters on `modeName === "Shortcut Remote command"`,
polls `meta.json` for up to 30 seconds for `rawResult` to populate
(SuperWhisper writes the file empty first and fills in the transcript
later — the gap can be several seconds), then routes the transcript
through `triggerVoiceCommand`. As a side effect it clears the K11
helper's lock file at `/tmp/sr-voice-recording` and restores the user's
previous SuperWhisper mode (saved by the helper to `/tmp/sr-prev-mode`)
so K7 / normal dictation isn't stuck in voice-command mode after the
LLM round-trip completes.
This replaces the per-mode `script` field that SuperWhisper 2.13 dropped
from its UI. We don't depend on the script field at all anymore;
SuperWhisper just records and writes its standard meta.json, and the
adapter does the rest.
### Defensive hardening
* `synthesizeKey` defaults `modifiers` to `[]` when missing. An LLM
-issued binding without modifiers crashed the daemon mid-session via
the HID event handler propagating the throw uncaught.
* `performAction` wrapped in try/catch; wheel-binding dispatch
self-heals on error (clears the binding + posts an HUD annotation
explaining why) so a bad binding doesn't lock the wheel into a crash
loop.
* Process-level `uncaughtException` and `unhandledRejection` handlers
log loudly but stay alive — so a future malformed binding (or any
other sync throw we missed) can't take down the HID handle and force
a launchd respawn cycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ering
HUD overhaul matching the rebuilt adapter. The keypad page now reflects
the adapter's authoritative state (layer, wheel value, wheel binding,
LLM-posted annotations) and exposes server lifecycle controls that
replace the retired menu-bar Kill Switch app.
## What landed
### Stable-height key cards
Every key card is now fixed at 92px regardless of layer or whether it
has a binding. No more layout jitter as you switch between layers with
different binding-text lengths.
### Layer banner with flash-on-change
Big monospace "Active Layer X" pill at the top of the keypad panel.
Flashes cyan when the layer changes, with a "← K2" caption so it's
clear the device (not the mouse) just changed it. Layer updates are
driven by `layer_change` WS events, not local clicks — clicking a
layer pill still works as a manual override / preview.
### Special-key rendering
K1 / K2 / K7 / K8 / K11 are rendered as "special" with hardwired labels
that never change with layer. Two visual treatments:
* **Pinned** (purple, K1 / K7 / K8 / K11) — hardwired action across all
layers
* **Layer-action** (blue, K2) — advances the layer
K3-K6, K9, K10 still cycle their labels per-layer from xppen-mappings.
### "AI Dial" panel
* Big monospace number for the wheel value, flashes purple on each tick
* Wheel-binding label (shows current binding, e.g. "Vol +6%·-6%", or
"AI input — no keystroke synthesis" when null)
* Annotation overlay box that renders payloads posted via
`post_annotation` (text + key/value fields + style: info/warn/error)
* Voice-command test input + Send button that POSTs to /voice-command
so the loop can be exercised without SuperWhisper
### Server panel
Three buttons that POST to `/control/{restart,stop,disable}` with
appropriate confirm dialogs. Status line tells you what was sent. Stop
and Disable both have hover hints explaining the difference (this
session vs persistent across logins) and the exact terminal command
to re-enable after Disable.
### Focus-notification opt-in for K1 → reload-on-K1
When K1 is pressed and the HUD is already open in a foreground tab,
the adapter sends a `hud_focus` WS event instead of running `open URL`,
which would otherwise spawn a duplicate browser tab. The HUD page
listens and reloads itself.
For backgrounded tabs (where in-page JS can't focus the tab without a
user gesture), there's a "🔔 Enable focus notifications" button. Once
the user grants permission once, K1 fires a system notification
("K11 pressed. Click to bring the HUD forward.") whose onclick handler
calls `window.focus()` and reloads. Falls back gracefully (silent
reload-on-return via visibilitychange) if permission is denied or
unsupported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Start/stop toggle for the SuperWhisper voice-command flow, called from the adapter as K11's hardwired shell-command action. ## How it works * **First press** (no lock present): saves the current SuperWhisper active mode to `/tmp/sr-prev-mode`, switches to the "Shortcut Remote command" mode via `superwhisper://mode?key=custom`, fires `superwhisper://record` to start recording, drops a lock file at `/tmp/sr-voice-recording`. * **Second press** (lock present, < 60s old): synthesizes the SuperWhisper toggleRecording hotkey (default Opt+Cmd+3) via osascript to stop the in-progress recording. SuperWhisper then transcribes and writes its standard `meta.json`, which the adapter's recordings-folder watcher picks up — the watcher is what clears the lock file and restores the previous SuperWhisper mode after the voice-command pipeline has run claude -p and reconfigured the keypad. * **Stale lock** (> 60s old): assumed dead from a cancelled recording; removed and treated as "not recording", so the next press starts fresh. ## Why URL scheme + hotkey instead of just a hotkey SuperWhisper 2.13 has no per-mode hotkeys (the entire Script section of the UI was removed in the 2.10 → 2.13 update). The only way to record in a *specific* mode is to switch to that mode first, then fire toggleRecording. URL scheme + hotkey is what the SuperWhisper docs steer toward (their example Apple Shortcut does the same dance). ## Configuration Env vars (all optional): SR_VOICE_MODE_KEY mode key (default: custom) SR_PREV_MODE_FILE prev-mode storage (default: /tmp/sr-prev-mode) SR_LOCK_FILE lock path (default: /tmp/sr-voice-recording) SR_LOCK_TTL seconds before lock is stale (default: 60) SR_TOGGLE_KEYCODE mac key code for toggleRecording (default: 20 = "3") SR_TOGGLE_MODIFIERS AppleScript modifier list (default: "option down, command down") Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## README rewrite The architecture diagram and operations table were both rewritten to reflect the unified one-process design. New sections cover: * Voice control with SuperWhisper (URL scheme + recordings-folder watcher pipeline; replaces the obsolete "set the mode's script field" instructions, which no longer work in SuperWhisper 2.13) * HUD as the control surface (Restart / Stop / Stop & disable buttons, with the exact re-enable command for the disable case) * Terminal escape hatches for when the HUD is unreachable * Layer behavior (which keys are hardwired across layers vs per-layer) * Install via `~/.claude.json` HTTP transport entry (was stdio) The directory layout was updated too, marking sidecar/ as RETIRED 2026-05-02 and noting the kill-shortcut-remote-mcp.sh CLI as the terminal escape hatch. ## handoff/ folder Three new dated docs for whoever picks up this work next: * **2026-05-03-handoff.md** — current state of the system, the voice loop architecture, and 13 landmines from this session (SuperWhisper 2.13 quirks, claude -p argv ordering, brightness key codes being no-ops, the F-key-above-F15 hotkey convention, wheel-binding action shapes, lock-file lifecycle, etc.) * **2026-05-03-next-brightness-via-betterdisplay.md** — concrete plan for the next task: bind the wheel to control screen brightness via the newly-installed `betterdisplaycli`, with the same Tink-tick feedback as the existing volume binding. Step-by-step, expected duration 15-30 min. * **2026-05-03-optional-followups.md** — three things future-Claude *might* want to do but doesn't have to: commit-splitting rationale (effectively retroactive — see the 6-commit shape that produced this), file unfinished vision threads as design issues (configuration-tool rename, floating HUD, layered AI semantics, per -key bindings, etc.), and cross-link the two issues filed during this session (`MCPAQL/adapter-generator#30` for OAuth scaffolding, `MCPAQL/adapter-studio` repo for the self-service vision). * **README.md** in handoff/ — index pointing at the three. The pre-existing project-root HANDOFF.md from 2026-04-28 is left in place as historical context — the new handoff/README explicitly notes it's superseded by the 2026-05-03 docs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six occurrences of `/Users/mick/...` across one Swift file and three handoff docs replaced with home-relative or placeholder forms. The repo is public and these paths exposed the developer's username + home directory layout — not a security issue (the username is already public on the GitHub account that owns the repo) but worth tidying for anyone else cloning. ## Changes * **tools/ShortcutRemoteKillSwitch.swift** — `rootPath()` fallback now uses `NSHomeDirectory()` to compute the conventional checkout path under the running user's home, instead of hardcoding mick's. Added a comment pointing to `tools/build-kill-switch-app.sh` as the proper way to inject the real path via the `ShortcutRemoteMCPRoot` Info.plist key. The fallback only matters when the .app is run without that key set (development / manual builds). * **handoff/2026-05-03-next-brightness-via-betterdisplay.md** — example MCP `set_wheel_binding` payload now uses `$REPO_ROOT/tools/...` as shorthand placeholder, with a sentence noting the `command` field needs the actual absolute path at runtime. * **handoff/2026-05-03-handoff.md** — `/Users/mick/Applications/...` → `~/Applications/...` for the retired menu-bar Kill Switch app references. * **handoff/2026-05-03-optional-followups.md** — same `/Users/mick/...` → `~/...` swap. Verified: `grep -rn "/Users/mick" generated/shortcut-remote-mcp/` returns nothing after this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ness
Adds tools/wheel-brightness-step.sh — a defensive wrapper around
betterdisplaycli for use as a `command`-shape wheel binding action.
Defensive design (lessons from the 2026-05-03 incident where
BetterDisplay crashed mid-session and turned the wheel into a beep
machine):
- pgrep precheck: skip the CLI call entirely if BetterDisplay isn't
running, avoiding multi-second IPC timeouts per wheel tick
- perl-alarm timeout (2s default) wraps the CLI call so a wedged-but-
running BetterDisplay can't pile up a backlog
- Tink audio feedback fires only on success — silence is the right
signal that something is wrong; a beep with no observable change
misleads, and queued audio after a fast-spin against a wedged
daemon creates a runaway phantom-beep loop
Tunable via env vars: BD_CLI, BD_APP, BR_STEP, BR_TARGET,
BR_CALL_TIMEOUT. Default target is -displayWithMouse (knob-feel:
turn the wheel while looking at the display you want adjusted).
Wired via mcpaql_update set_wheel_binding with action shape
{ command: <this script>, args: ["up"|"down"] }.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ements
Extends the `claude -p` voice-loop allowlist with three DollhouseMCP
patterns (`mcp__dollhousemcp__mcp_aql_{read,create,update}`) so voice
commands can compose keypad reconfiguration with Dollhouse element
activation in one shot. Activation is a read-state operation in
DollhouseMCP semantics; create lets voice synthesize new elements;
update modifies existing element parameters.
Delete and execute deliberately deferred (#34) — execute opens
agentic chain-spawning (an activated agent can run a Claude session
that goes off and does arbitrary work) and needs deliberate gating.
Delete is too easy to mis-transcribe. The deferred-opt-in path will
land via the research-mode RFC (forthcoming) which establishes a
broader allowlist tier engaged via an explicit gesture (chord or
multi-tap) and a required-persistence contract.
VOICE_SYSTEM_PROMPT updated with a Dollhouse recipe block: explains
the three allowed tools, calls out the activate-is-read semantic,
instructs the LLM to refuse + annotate if asked for delete/execute,
and requires a single combined annotation when the voice command
spans both a keypad change and a Dollhouse change.
Allowlist remains overridable at runtime via VOICE_ALLOWED_TOOLS env
var on the launchd plist.
Closes #34. Part of #39 (shortcut-remote-mcp epic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reference implementation of the MCPAQL pattern applied via JIT discovery to an undocumented vendor-locked HID device — the Hanvon Ugee Shortcut Remote (XP-Pen Shortcut Remote, USB
0x28bd:0x0202). No vendor SDK, no Ghidra; just observational HID capture → decoded protocol → CRUDE adapter + live HUD + sidecar.This PR contains:
PASSTHROUGHextended to support shell-command actions in addition to keystroke synthesis. Default is now empty so users wire only what they need.osascriptsynthesis hit interface 2's bit transitions, doubling the trigger.SHORTCUT_REMOTE_AUTO_OPEN=1env var (this commit) — adapter opens the HID device at startup so a standalone sidecar receives HUD WebSocket events immediately, without needing an MCP host to first call a read op.HANDOFF.md(this commit) — session-continuity doc that was generated during the original build but never committed.Why this is a "JIT" adapter
This adapter was built without any vendor cooperation:
tools/capture-hid.js).docs/xppen-decode-reference.md).adapter/src/server.js+schema.json+provenance.json).The capture artifacts (
capture/raw-reports.jsonl,capture/discovery-bundle.json) are checked in for reproducibility — anyone can re-derive the schema from those reports.CRUDE surface (
adapter/src/schema.json)mcpaql_read:list_devices,get_device_info,get_battery_status,get_button_state,wait_for_button_press,get_recent_events,is_device_open,get_hud_urlmcpaql_delete:release_devicePlus a live HUD at
http://127.0.0.1:47832/with a WebSocket events stream that the sidecar consumes.Pitfalls captured in HANDOFF.md
node-hidopens withkIOHIDOptionsTypeSeizeDeviceon macOS — be careful which interface you open (vendor page only, by default; opening interface 0 steals keystrokes from XP-Pen daemon).<ACK05>(internal model code), not<ShortcutRemote>(a stale legacy template).hud.html,xppen-mappings.json) are read on-demand per HTTP request so edits hot-reload without restarting the MCP server.Test plan
cd generated/shortcut-remote-mcp/adapter && npm installSHORTCUT_REMOTE_AUTO_OPEN=1 node src/server.jshttp://127.0.0.1:47832/— should show live keypad statecd ../sidecar && npm install; copy a sample PASSTHROUGH entry intoindex.js; runnode index.js; verify the bound action fires on the press edgerelease_deviceis called🤖 Generated with Claude Code