Hand Control for developers and AI agents.

Version 1.2.5 Claude, other agents, scripts and your own programs on the same PC can see whether Hand Control is running, read the hands it is following, and follow every gesture, click, key press and scroll. With your permission they can also turn control on or off, choose the 3D program profile, or take a camera snapshot. They cannot start any mouse or keyboard input: only your hands do that.

MCP HandControlMCP.exe over stdio HTTP 127.0.0.1:47294, this PC only Default off until you tick it
Start here

How an agent reaches Hand Control

Everything stays on your PC, from your agent to the app:
  1. Your agent Claude or another MCP client Claude Code, Claude Desktop, or any client that runs a stdio server.
  2. The connector HandControlMCP.exe Your agent runs it as an MCP server over stdio. Installed next to the app; finds the port and the key by itself.
  3. The local API 127.0.0.1:47294 The connector calls it over HTTP, signed with the key. This PC only; web pages are refused, even with the key.
  4. The app Hand Control Answers only what you allowed, and nothing until you tick it in the Agents tab. Follows your hands, 21 points each.

The path ends at the app. An agent reads hands, gestures, clicks, key presses and scrolls and, if you allow it, turns control on or off, picks the 3D program or takes a snapshot. It cannot start any mouse or keyboard input: only your hands do that. Turning control off lets go of what your hands are holding; switching the program in the middle of a two-hand gesture lets go too, and the gesture then carries on with the new program’s buttons.

Your own scripts skip the connector and call 127.0.0.1:47294 directly: see the local API.

What an agent reads when you point and pinch. Your hand makes the click; the agent only sees it afterwards, as events from get_events, numbered here in the order they arrive.
  1. You pointgesture point
  2. You pinch thumb and indexsnap_back px only if the pointer drifted as the fingers closed: it is put back before the press
  3. button L down at pos [x, y]
  4. gesture left down while you keep pinching (move to drag)
  5. You let gobutton L up
  6. gesture point back to pointing

What an agent can and cannot do

An agent can

  • See whether Hand Control is running and whether control is on
  • Read the hands it is following (with Only me on, only yours): 21 points per hand, fingers up
  • Wait for a gesture, or read the stream of gesture, click, key and scroll events

Only if the user ticked it

  • Turn control on or off
  • Switch the 3D program profile
  • Take a camera snapshot

An agent can never

  • Start any mouse or keyboard input (move, click, scroll or type)
  • Connect from another computer
  • Get your face signature or settings file from Hand Control (the API and its tools never return them)

All of this happens on your PC. The connector talks to Hand Control over 127.0.0.1 and nothing leaves the machine. Anything the agent then does with what it read, such as sending it to its own model, is up to that agent and its app.

Get Hand Control

Install Hand Control, then turn on the agent API in the Agents tab as described in step 1.

Back to the contents

Claude and other MCP clients

Connect Claude in three steps

Step 1: Turn on the agent API

In Hand Control, open the Agents tab and tick Allow local programs to connect (agent API). Tick the two permission boxes only if you want an agent to be able to do those things.

Step 2: Connect your agent

Hand Control installs a connector, HandControlMCP.exe, next to the app. It speaks the Model Context Protocol, both the current revision (2026-07-28) and the older handshake-based revisions, so older and newer clients both work. It finds the port and the access key by itself.

The Agents tab has two buttons that copy the exact text for your install:

Claude Code

Copy for Claude Code, then paste it into a terminal. It looks like this:

Terminal
claude mcp add --scope user --transport stdio hand-control -- "C:\Users\<you>\AppData\Local\Programs\Hand Control\HandControlMCP.exe"

--scope user makes it available in every project. Check it with claude mcp list.

The Agents tab in Hand Control. Allow local programs to connect (agent API) is ticked, and the line under it reads On - 127.0.0.1:47294 (this PC only). The What agents may do line says: Always: read status, hand positions, gestures and each click, key and scroll Hand Control sends. They can never move the mouse. Below: two unticked permission boxes, Copy key and New key buttons, and Copy for Claude Code and Copy for Claude Desktop buttons.
The Agents tab in Hand Control 1.2.5, with the agent API on. Select the picture to open it full size.

Claude Desktop

Copy for Claude Desktop. In Claude Desktop, open Settings > Developer > Edit Config, which opens %APPDATA%\Claude\claude_desktop_config.json. Merge the copied block into mcpServers:

JSON
{
  "mcpServers": {
    "hand-control": {
      "command": "C:\\Users\\<you>\\AppData\\Local\\Programs\\Hand Control\\HandControlMCP.exe",
      "args": []
    }
  }
}

Quit Claude Desktop completely and start it again.

Any other MCP client

Run HandControlMCP.exe as a stdio server with no arguments. It reads api_port from %APPDATA%\Hand Control\settings.json when it starts, so restart your agent after changing the port; --port <n> (1024–65535) forces a port; any other value is ignored. --version prints the version. --selftest checks the exe and exits.

Without MCP

Scripts and your own programs can use the HTTP API directly; see the local API reference.

Step 3: Try it

Ask your agent, for example:

  • “Is Hand Control on? Which program is it set up for?”
  • “Wait until I make the rotate gesture, then tell me.”
  • “Switch Hand Control to Fusion 360.” (needs the control permission)
  • “Look through the Hand Control camera: is my hand in view?” (needs the camera permission)

Troubleshooting

What to do when the agent reports a problem
The agent saysDo this
“...agent API has never been turned on...”Tick the box in step 1. That also creates the key.
“Hand Control is not running, or its agent API is off”Start Hand Control. Check the Agents tab shows On.
“Could not verify that the program on Hand Control's port is Hand Control...”Another program has the port, or the key file does not match the running app. Restart Hand Control. The key was not sent.
“Hand Control is busy (too many programs are asking at once)...”More than 8 connections were open at once. Try again in a second.
“busy: at most 8 requests at once - retry shortly”The agent sent more than 8 tool calls at once to the connector. Try again.
“...is off. The user can allow it in Hand Control > Agents...”That action needs its tick box.
The tools do not appear in Claude DesktopRestart it fully. Logs are in %APPDATA%\Claude\logs\mcp-server-hand-control.log.

To shut every program out at once, untick the box in step 1. New key stops any program that kept a copy of the old key; a program that reads the key file each time, such as this connector, keeps working.

Back to the contents

CAD and modelling

Two-hand gestures in 3D programs

Hand Control drives Windows with ordinary mouse input, so most desktop programs work with it: CAD, modelling tools, browsers and slide shows. You do not install a plug-in into the program. Programs that run as administrator need Hand Control to run as administrator too (see below), and games that read raw mouse input may ignore the pointer movement.

The one-hand gestures (point, pinch to click and right-click, V sign to scroll; or the finger-tap style) behave the same everywhere. The two-hand gestures (rotate, pan, zoom) send the view controls of the program you choose in Gestures > Two-hand gestures drive.

Supported 3D programs

What each profile’s two-hand gestures send, with the source for the default bindings
ProfileTwo-hand rotate sendsTwo-hand pan sendsTwo-hand zoom sendsSource for the default bindings
SolidWorksmiddle-dragCtrl + middle-dragwheelSOLIDWORKS Help: Middle Mouse Button Functions (middle = rotate, Ctrl + middle = pan, wheel = zoom)
Fusion 360Shift + middle-dragmiddle-dragwheelFusion Help: Fusion preferences reference (Fusion default: Shift + middle = orbit, middle = pan, wheel = zoom)
Blendermiddle-dragShift + middle-dragwheelBlender manual, 3D Viewport navigation
Onshaperight-dragmiddle-dragwheelOnshape Help: View navigation and the View Cube (default: right-drag = rotate, middle-drag = pan, wheel = zoom)
Hand Control demos / web 3Dmiddle-dragCtrl + middle-dragwheelThe Hand Control demos

The bindings were checked against the sources above on 26 Sep 2026 (Fusion 360 again on 28 Sep 2026). If you have changed a program’s mouse settings, pick the profile whose controls match yours.

How the two-hand gestures work

The three two-hand gestures and what Hand Control does for each
GestureHandsWhat Hand Control does
RotateBoth hands pinch, then move both the same wayHolds the profile’s rotate button and keys, and moves the pointer with your hands.
ZoomBoth hands pinch, then pull apart or bring closerLets go of every held button and key, sends plain wheel notches (at most 3 per frame), then takes them again.
PanBoth hands make fists, then move both the same wayHolds the profile’s pan button and keys, and moves the pointer.

Safety behaviour, which is the same in every profile:

  • Everything held is released when a hand leaves view, when the camera stalls, when the camera or Mirror changes, and when you are no longer recognised with Only me on.
  • Switching the profile in the middle of a gesture releases everything and takes the new controls.
  • If Windows refuses to release a button (it can when the program in front runs as administrator), Hand Control sends nothing else until that release goes through.
  • Ctrl+Alt+H switches control on or off while the camera is running, whatever your hands are doing.

Programs that run as administrator

Windows does not let a normal program send input to a program running as administrator (User Interface Privilege Isolation). If Hand Control does nothing in one program, check whether that program was started “as administrator”. Either start it normally, or start Hand Control as administrator too.

Programs without a profile

Most 3D programs let you choose mouse controls. Pick the preset that matches one of the profiles above; the SolidWorks or Blender styles are the most common. The profile is saved in %APPDATA%\Hand Control\settings.json as app_profile.

Connecting other software

Which part of Hand Control to use for each kind of connection
You want toUse
Let Claude or another AI agent see your gesturesConnect Claude (MCP connector)
React to gestures from your own script or appthe local API reference: /v1/events long-poll, /v1/hands landmarks
Switch the profile automatically when a program comes to the frontA small script that watches the foreground window and calls POST /v1/profile (needs the control permission)
Try it in a browserHand Control demos

Back to the contents

HTTP API, version 1

The local API

Hand Control can let other programs on the same PC see what it sees: whether it is running, the hands it is following (21 landmarks each), and every gesture and input event. With the user’s permission, a program can also turn control on or off, choose the 3D program profile, or take a camera snapshot.

The API cannot start any mouse or keyboard input: only the user’s hands do that. Turning control off, or switching the 3D program in the middle of a two-hand gesture, lets go of what the hands are holding (a program switch then takes the new program’s buttons for the same gesture).

If you are connecting an AI agent (Claude Code, Claude Desktop or any other MCP client), you do not need this page: use the MCP connector described in Connect Claude. This page is for scripts and your own programs.

Turning it on

The API is off until the user turns it on:

  1. Open Hand Control and go to the Agents tab.
  2. Tick Allow local programs to connect (agent API). The line under it shows On - 127.0.0.1:47294 (this PC only).
  3. Optional, off by default:
    • Let agents turn control on/off and change the program enables POST /v1/control and POST /v1/profile.
    • Let agents take a camera snapshot (shows you) enables GET /v1/camera.jpg.

The API starts with the app from then on, until the box is unticked. If it cannot start, for example because another program already uses the port, Hand Control shows an error and unticks the box itself.

Connecting

Address, key and request rules for the local API
Addresshttp://127.0.0.1:47294. The port is api_port in %APPDATA%\Hand Control\settings.json (1024–65535). Use 127.0.0.1 rather than localhost: the server listens on IPv4 only, and localhost can resolve to IPv6 ::1 first.
KeyThe file %APPDATA%\Hand Control\api-key.txt (43 characters). Copy key in the Agents tab copies the same key. New key replaces it, and the old key stops working at once. The file is in your own profile, so any program running as your Windows user can read it (the MCP connector reads it on every call); to shut every program out, untick Allow local programs to connect.
Every requestHost: 127.0.0.1:<port> (or localhost:<port>), and either a signed Authorization: HC-HMAC-SHA256 ... header (recommended: the key never leaves your program, see below) or Authorization: Bearer <key> (simplest).
BodiesJSON objects of at most 4 KB, with Content-Type: application/json and a Content-Length (no chunked bodies).
RepliesJSON (the snapshot is a JPEG), with Cache-Control: no-store. Three kinds differ: the busy 503 (empty, see below), the plain HTML errors that the HTTP layer itself sends for a request it cannot read, which carry no Cache-Control, and the reply to a GET with no HTTP version, which has no headers at all. One request per connection (HTTP/1.0).
What is refused, and why
Requests the API refuses, the status it returns, and why
RefusedStatusReason
No key or wrong key, a bad signature, one whose time is more than 60 s off the PC’s clock (either way), a signature used twice401Keeps out web pages and programs of other Windows accounts, which cannot read your key file; a signed request cannot be replayed.
Host is not 127.0.0.1:<port> or localhost:<port>403 bad_hostStops DNS-rebinding: a web page pointing its own domain at 127.0.0.1.
Any Origin or Sec-Fetch-Mode header403 browser_refusedWeb pages may never call it, even with the key.
An action the user has not allowed403 not_allowedThe message names the tick box.
More than 8 connections at once503Straight away, with an empty body and Retry-After: 1, and not signed. Retry after a second.
More than 4 waiting events requests429So status calls always get through.
A client that stalls mid-request, or takes more than 10 s to send itdropped
The user turns the API off (the tick box, or Reset ALL settings), withdraws a permission, or makes a New key while a request is running503 api_off, 403, 401, or the connection is closedOpen connections are cut, and an action is re-checked when it runs: the API still on, still allowed, and the request’s key still the key.

The server listens on 127.0.0.1 only. While it runs, no other program can take connections meant for it: Windows refuses a second bind of 127.0.0.1 on that port, and a program listening on 0.0.0.0 with the same port does not receive connections to 127.0.0.1 (both measured on Windows 10, and re-checked by the test suite).

Signed requests (recommended): the key never leaves your program

When Hand Control is closed, any other program could open port 47294. A Bearer header would hand that program the key. With a signed request it only sees a one-off signature, and every reply to an accepted signed request is signed back, so you also know the answer came from Hand Control, on the same connection. This is what the MCP connector does.

Signature format
Authorization: HC-HMAC-SHA256 ts=<unix seconds>, nonce=<32 lowercase hex>, sig=<64 lowercase hex>

sig = hex HMAC-SHA256(key, the six lines below joined with "\n" (no newline at the end))
      hand-control-request
      METHOD                        GET or POST
      TARGET                        path + query, exactly as sent
      ts                            the same number as in the header
      nonce                         the same hex as in the header
      hex SHA-256 of the body       of the empty string for GET
  • TARGET is the path with its query string exactly as sent (for example /v1/events?after=3&wait=25).
  • ts must be within 60 s of the PC’s clock.
  • Each nonce works once (use 16 random bytes).
  • The reply carries X-HandControl-Signature = hex HMAC-SHA256(key, these four lines joined with \n: hand-control-response, the nonce, the HTTP status, hex SHA-256 of the reply body). If it is missing or wrong, do not trust the reply. A refusal made before the signature is checked is not signed: a wrong Host, a browser, a chunked body or a missing Content-Length (411), a body shorter than its Content-Length (400) or larger than 4 KB (413), a bad, stale or replayed signature (such as one made with a wrong key), the 503 sent when more than 8 connections are open, and the plain HTML replies below. A GET sent with no HTTP version is not signed either, even when it is accepted: its reply has no headers at all (see Endpoints).

Bearer requests get no reply signature. If you use Bearer, you can at least check the server first with GET /v1/hello?challenge=<32-128 lowercase hex>, the one request that needs no key. It returns proof = hex HMAC-SHA256(key, "hand-control-hello:" + challenge). This does not protect against the port changing hands between that check and your request; signed requests do.

The endpoints, their replies and two worked examples are in the Endpoints tab.

Back to the contents

HTTP API, version 1

Endpoint reference

Open an endpoint to see its reply and fields. Every request except GET /v1/hello needs the key, sent as described in Connecting.

How errors come back:

  • JSON: every error has the shape {"error": {"code": "...", "message": "..."}}, except the three below.
  • Too many connections: with more than 8 open, the 503 has an empty body and Retry-After: 1.
  • Unknown method: a method other than GET, POST, PUT, DELETE and OPTIONS gets a plain HTML 501.
  • Unreadable request: a plain HTML 400 (414 for a request line over 64 KB, 431 for 100 or more header lines or a header line over 64 KB).
  • No status line or headers: when the request line has no HTTP version, a malformed one, or HTTP/2.0 or later, the HTML page is sent alone, so only its text names the error: 400, or 505 for HTTP/2.0 or later. The exception is a GET with no HTTP version (GET /v1/status): it is still answered, as HTTP/0.9, so its body arrives alone, with no reply signature.
GET /v1/status
JSON reply
{
  "app": "Hand Control", "version": "1.2.5", "api": 1,
  "camera_running": true, "camera_connected": true, "fps": 30.0,
  "control": true, "gesture": "point",
  "hands": ["Right"], "hands_seen": 1,
  "only_me": true, "owner": "recognised",
  "profile": "solidworks",
  "allowed": {"control": false, "camera": false},
  "events": {"stream": "9f2c41ab", "latest": 118}
}
  • control: whether the hands are driving the mouse right now.
  • hands: labels of the hands that may drive input. With Only me on, only the enrolled user’s hands are listed.
  • hands_seen: every hand in view (at most 2), including ignored ones.
  • owner: with Only me on, one of "starting", "no face enrolled", "recognised", "looking for you" (a face is in view but not recognised as yours) or "cannot see your face"; null while Only me is off or the camera is stopped.
  • events.latest: the newest event id. Start /v1/events from it.
GET /v1/hands

The hands that are allowed to drive input, from the latest camera frame. Returns 409 camera_stopped if the camera is off.

JSON reply
{
  "time": 1790446131.371, "age_s": 0.03, "frame": 5120,
  "hands": [
    {"label": "Right",
     "landmarks": [[0.512, 0.604], [0.498, 0.571], "... 21 points ..."],
     "fingers_up": ["index"]}
  ]
}
  • landmarks: the 21 MediaPipe hand landmarks in order. 0 is the wrist; 4, 8, 12, 16 and 20 are the thumb, index, middle, ring and pinky tips. x and y run across the camera image as the preview shows it (mirrored if Mirror is on), normally from 0 to 1; they are passed through unclamped, so they go slightly outside at the frame edge.
  • label: Left or Right. With Only me off it is MediaPipe’s handedness guess; with Only me on it is the side of your body the hand belongs to, from the body pose.
  • age_s: seconds since that frame. It grows if the camera stalls.
  • fingers_up: any of index, middle, ring, pinky. The thumb is never listed.
GET /v1/events?after=<id>&wait=<seconds>&limit=<n>&stream=<stream>

Events newer than after, in order. With wait (0–25 s), the call waits until at least one event arrives, which makes it a long poll.

JSON reply
{"stream": "9f2c41ab", "restarted": false, "next": 121, "latest": 121, "more": false, "missed": false, "events": [
  {"id": 119, "time": 1790446131.9, "type": "button", "button": "M", "action": "down", "pos": [812, 440]},
  {"id": 120, "time": 1790446131.9, "type": "gesture", "gesture": "rotate", "previous": "two hands"},
  {"id": 121, "time": 1790446131.9, "type": "two_hand_start", "mode": "rotate"}
]}
  • Loop with after = next from the last reply. next is the id of the last event in this reply; latest is the newest id there is. When more events are buffered than limit, more is true and the next call returns the rest straight away. Never jump to latest, or you skip that page. When no event arrives before wait runs out, next is your after (or latest, if your after was beyond it), so the loop simply asks again.
  • stream changes every time Hand Control starts. Send back the stream of your last reply: if Hand Control has restarted since, your cursor means nothing any more, and the reply comes at once, from the start of the new run, with restarted: true, even if you asked it to wait. Without stream, an old cursor above the new run’s latest returns only what arrives after it.
  • missed: true means events after your after were already dropped from the 500-event buffer.
  • limit is 1–500 (default 100).
Event types, their fields, and when each is sent
typeFieldsWhen
gesturegesture, previousThe active gesture changed.
buttonbutton (L/R/M), action (down/up), pos [x, y]A mouse button went down or up.
keykey (Ctrl/Shift), actionA modifier key went down or up.
scrollnotches, whyMouse wheel.
controlenabled, sourceControl turned on or off. source is rock sign, Ctrl+Alt+H, app button (the button in the app) or agent API.
two_hand_start / two_hand_endmode (and why when a profile change ended it)Two-hand rotate, pan or zoom began or ended.
click_refusedbutton, reasonA tap or pinch was not accepted as a click.
snap_backpxA pinch click put the pointer back by px pixels (the drift of closing the fingers) before pressing.
hands_lostheldEvery hand left view (after the short dropout grace). Anything held was released.
hand_switchprevious, now, jumpA different hand took over the pointer. Anything held was released.
two_hand_jumpmid, spanA tracked hand jumped during a two-hand gesture. The gesture takes a new starting point instead of turning or zooming by the jump.
tracking_resetwhyThe camera source or Mirror changed, or you stopped being recognised with Only me on. Anything held was released.
camera_stallheld, secondsNo camera frame arrived for a moment while something was held. Anything held was released.
input_refusedwhatWindows refused a press or a release (it can when the program in front runs as administrator). A refused release is retried until it goes through.
input_blockedwhat, pendingA press, key or scroll was not sent because an earlier release is still waiting to go through.
input_stuckheldHand Control stopped with a button that Windows would not release.

Gesture names are point, left down, right down, scroll, open hand (lifted), rock, two hands, rotate, pan, zoom, none, off and waiting for the previous release. While control is off, only none, rock, off and waiting for the previous release are reported. /v1/hands still works then, so you can recognise poses yourself. With the default settings two pinching hands report rotate, and pulling them apart or together zooms within it (scroll events with why two-hand zoom); zoom is reported only when Two-hand pinch = rotate is off in the Gestures tab.

GET /v1/profiles

The 3D programs the two-hand gestures can drive, and the controls each one sends.

JSON reply
{"current": "solidworks", "profiles": [
  {"id": "solidworks", "name": "SolidWorks", "rotate": "middle-drag", "pan": "Ctrl+middle-drag", "zoom": "wheel"},
  {"id": "fusion360", "name": "Fusion 360", "rotate": "Shift+middle-drag", "pan": "middle-drag", "zoom": "wheel"}
]}
POST /v1/control needs “Let agents turn control on/off and change the program”
Request and reply
{"enabled": true}   ->   {"control": true}

This sets control on or off; it does not flip it. It goes through the same lock as the rock sign, so the two never race. Returns 409 if the camera is stopped.

POST /v1/profile needs “Let agents turn control on/off and change the program”
Request and reply
{"profile": "fusion360"}   ->   {"profile": "fusion360"}

The profile is one of solidworks, fusion360, blender, onshape, browser3d. It is saved, and the Gestures tab updates. If the user is in the middle of a two-hand gesture, everything held is released and taken again with the new keys.

GET /v1/camera.jpg needs “Let agents take a camera snapshot (shows you)”

One JPEG of the camera image. While the preview is on it is the preview, with the tracked hands drawn on it; with the preview off it is the plain camera frame. It is made in memory and never saved to disk, and is never older than 2 s: without a recent frame it returns 503 no_frame.

GET /v1/hello?challenge=<32-128 lowercase hex>

The one request that needs no key. It lets a program check that it is talking to Hand Control before it sends the key (see the Bearer note under Signed requests).

JSON reply
{"app": "Hand Control", "version": "1.2.5", "api": 1, "proof": "<64 lowercase hex>"}

proof = hex HMAC-SHA256(key, "hand-control-hello:" + challenge). Returns 400 bad_argument if the challenge is not 32–128 lowercase hex, and 503 no_key if the API has no key yet.

Examples in Python and PowerShell

Python (standard library only; signed requests, replies verified):

Python
import hashlib, hmac, json, os, secrets, time, urllib.error, urllib.request

KEY = open(os.path.expandvars(r"%APPDATA%\Hand Control\api-key.txt")).read().strip()
BASE = "http://127.0.0.1:47294"
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))   # never through a proxy

def mac(*parts):
    return hmac.new(KEY.encode(), "\n".join(parts).encode(), hashlib.sha256).hexdigest()

def call(method, target, body=None):
    data = json.dumps(body).encode() if body is not None else b""
    ts, nonce = str(int(time.time())), secrets.token_hex(16)
    sig = mac("hand-control-request", method, target, ts, nonce, hashlib.sha256(data).hexdigest())
    headers = {"Authorization": f"HC-HMAC-SHA256 ts={ts}, nonce={nonce}, sig={sig}"}
    if body is not None:
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(BASE + target, data=data if body is not None else None,
                                 method=method, headers=headers)
    try:
        with opener.open(req, timeout=40) as r:
            status, got, raw = r.status, r.headers.get("X-HandControl-Signature", ""), r.read()
    except urllib.error.HTTPError as e:
        status, got, raw = e.code, e.headers.get("X-HandControl-Signature", ""), e.read()
    if status == 503 and not raw:
        raise RuntimeError("Hand Control is busy (more than 8 connections); retry in a second")
    if not got and status >= 400:
        # refused before the signature was checked (clock off, nonce reused, wrong key, ...): not signed,
        # so the reason it gives cannot be verified
        try:
            err = json.loads(raw)["error"]
            reason = f'{err["code"]}: {err["message"]}'
        except (ValueError, KeyError, TypeError):
            reason = "no JSON error"
        raise RuntimeError(f"refused before signing (HTTP {status}); unverified reason: {reason}")
    want = mac("hand-control-response", nonce, str(status), hashlib.sha256(raw).hexdigest())
    if not hmac.compare_digest(got, want):
        raise RuntimeError("not Hand Control (or the key was replaced)")
    if status >= 400:
        raise RuntimeError(json.loads(raw)["error"]["message"])
    return json.loads(raw)

ev = call("GET", "/v1/status")["events"]
after, stream = ev["latest"], ev["stream"]
while True:
    r = call("GET", f"/v1/events?after={after}&wait=25&stream={stream}")
    after, stream = r["next"], r["stream"]
    for e in r["events"]:
        if e["type"] == "gesture":
            print(e["previous"], "->", e["gesture"])

PowerShell:

PowerShell
$key = Get-Content "$env:APPDATA\Hand Control\api-key.txt"
Invoke-RestMethod http://127.0.0.1:47294/v1/status -Headers @{ Authorization = "Bearer $key" }

Versioning

Every path starts with /v1. New fields may be added to replies; clients should ignore fields they do not know. A breaking change will get a new version prefix.

Back to the contents

Written for the agent to read

For the agent

This part is addressed to the agent itself: point your agent at this section. A plain-text copy for agents, with how to connect, is at agents.txt (plain-text instructions for agents).

You are connected to Hand Control, a Windows app that turns the user’s webcam into a mouse. The user points to move the pointer, pinches thumb and index to click, pinches thumb and middle to right-click, and scrolls with a V sign and move. In the “Finger tap” click style they tap the index to click, make a V sign and tap the middle finger to right-click, and pinch and move to scroll. Two hands rotate, pan and zoom 3D views. Your tools observe this, and with permission adjust it. You cannot start any mouse or keyboard input. Do not offer to, and do not try to simulate input through these tools.

Tools

The tools the agent has, what to use each for, and notes
ToolUse it toNotes
get_statusCheck running, control on/off, current gesture, hands, profile, and your permissions (allowed)Call this first.
get_handsRead hand geometry: 21 landmarks (0 = wrist, 4/8/12/16/20 = fingertips), fingers_up, age_sx and y run across the camera image, normally 0 to 1 (slightly outside at the frame edge), mirrored if Mirror is on. Ignore readings with a large age_s.
get_eventsRead what happened since after: gestures, clicks, scrolls, modifier key presses (Ctrl/Shift), control changesPass the returned next back as after; more: true means another page is waiting. wait_seconds (up to 25) waits for the next event.
wait_for_gestureBlock until the user makes a gesture (for example rotate, pan, point, rock)With the default settings two pinching hands report rotate, and pulling them apart or together zooms within it; zoom is reported only when Two-hand pinch = rotate is off in the Gestures tab. While control is off only none, rock, off and waiting for the previous release are reported. Default timeout 30 s, maximum 120 s.
list_profilesSee which 3D programs are supported and the buttons and keys each one sends
set_controlTurn hand control on or offNeeds permission. Turning it ON makes the user’s hands move their mouse, so only do it when the user asked.
set_profileChoose solidworks, fusion360, blender, onshape or browser3dNeeds permission. Use it when the user says which program they are in.
camera_snapshotSee the camera image, with the tracked hands drawn on it while the preview is onNeeds permission. It shows the user; take one only when it helps with what they asked.

Rules

  1. Respect allowed. If an action tool returns an error saying it is off, tell the user which box to tick (the error names it). Do not retry in a loop.
  2. Ask before turning control on unless the user just told you to.
  3. Tool errors are instructions for the user, such as “start Hand Control” or “tick the box”. Pass them on plainly.
  4. Gestures while control is off: only none, rock, off and waiting for the previous release are reported. To react to hands while control is off, read get_hands and use fingers_up.
  5. The rock sign (index and pinky up for one second) is the user’s own on/off switch. Do not treat it as a command to you.
  6. Privacy: hands of other people are filtered out when the user has Only me on. Do not ask for snapshots you do not need.

Useful patterns

  • “Tell me when I rotate the model”: wait_for_gesture {"gesture": "rotate"}.
  • Count clicks for a minute: get_status gives events.latest = after. Then loop get_events {"after": after, "wait_seconds": 25} (then after = next), counting type == "gesture" events whose gesture is left down or right down, until 60 s have passed. Each click gives one, so a double-click counts as two. Do not count button events: two-hand rotate and pan hold a mouse button too (the right button in Onshape), and every zoom step lets it go and presses it again.
  • “Set it up for Onshape”: set_profile {"profile": "onshape"}, then tell the user that two-hand rotate now sends right-drag.
  • Is a hand held open? get_hands, then check fingers_up has all four fingers (index, middle, ring, pinky; the thumb is never listed).

The full HTTP reference behind these tools is the local API reference. The 3D program bindings and their sources are in the 3D programs tab.

Back to the contents

Agents watch.
Your hands drive.

Install Hand Control, then turn on the agent API in the Agents tab.