ElfDeckAI
Virtual keyboard, gamepad and mouse for AI agents on Linux and the Steam Deck.
ElfDeckAI lets an AI agent operate a real Linux machine through its input
devices. Three small daemons create virtual devices on /dev/uinput, and
an MCP server exposes them, together with a screenshot tool, as tools any
MCP client can call:
- ElfKey: a virtual keyboard
- ElfPad: a virtual Xbox 360-class gamepad (buttons, analog triggers, two sticks, D-pad)
- ElfMouse: a virtual mouse (relative pointer, buttons, wheel)
Applications, games, Steam Input and the compositor see these as ordinary hardware. Unlike emulator-scripting approaches, nothing depends on a particular emulator or game: ElfDeckAI has been used to play Pokémon FireRed in RetroArch on a physical Steam Deck from the intro through the first rival battle, with the agent seeing only screenshots and acting only through ElfKey. See notes/driving.md for what was learned.
The daemons and server are single-file Python programs that use only the standard library.
Quick start: Steam Deck
On the Deck, switch to Desktop Mode, open Konsole and run:
curl -fsSL https://raw.githubusercontent.com/Garthnesss/ElfDeckAI/main/install-on-deck.sh | sh
The installer works entirely inside your home directory (no sudo):
- installs the
elfdeckaipackage for your user, - installs and starts
elfkey,elfpadandelfmouseas systemd user services, - checks that all three virtual devices are present, and
- prints the MCP configuration for your PC and the commands to enable SSH.
It is safe to re-run; it upgrades in place.
For a guided setup, give AI_SETUP_GUIDE.md to your AI assistant. It is written for an assistant to follow and walks through the Deck side, the PC side and troubleshooting.
Quick start: any Linux machine
pip install elfdeckai
elfkey &
elfpad &
elfmouse &
echo "key x 150" > /tmp/elfkey.fifo # tap X for 150 ms
echo "press a" > /tmp/elfpad.fifo # tap A
echo "move 300 -100" > /tmp/elfmouse.fifo # move the pointer
echo "click left" > /tmp/elfmouse.fifo # left click
grep -E "ElfKey|ElfPad|ElfMouse" /proc/bus/input/devices # confirm the devices exist
Without pip, clone the repository and run python3 elfkey.py (and so on)
directly. Runtime state lives in /tmp and is cleared on reboot.
/dev/uinput is user-writable on the Steam Deck by default. On other
distributions, install the udev rule first (see Installation).
MCP server
elfdeckai-mcp is a Model Context Protocol server (stdio transport) that
exposes 21 tools:
| Group | Tools |
|---|---|
| Keyboard | key_tap, key_down, key_up, key_release |
| Gamepad | pad_press, pad_down, pad_up, pad_stick, pad_trigger, pad_center, pad_reset |
| Mouse | mouse_click_at, mouse_move_to, mouse_move, mouse_click, mouse_down, mouse_up, mouse_scroll, mouse_hscroll, mouse_release |
| Vision | screenshot |
screenshot captures the screen of the machine the server runs on and
returns a PNG, so an agent can work in a look, act, look again loop
without a separate video stream. It supports the Steam Deck in both Gaming
Mode (through gamescope) and Desktop Mode (KDE Plasma), wlroots compositors
and X11. Screens larger than
1280 pixels on their long side are scaled down before sending (set
ELFDECKAI_SCREENSHOT_MAX to change the limit, or 0 to turn it off), and
the result includes the image's size.
mouse_click_at and mouse_move_to take coordinates in the pixels of the
most recent screenshot, so an agent can click exactly what it sees, whatever
the screen's real resolution or the scaling applied. They assume a single
display.
Running on the same machine as the daemons:
{"mcpServers": {"elfdeckai": {"command": "elfdeckai-mcp"}}}
Running on a Steam Deck from an AI client on your PC, over SSH:
{"mcpServers": {"elfdeckai": {
"command": "ssh",
"args": ["deck@DECK-IP-ADDRESS", "/home/deck/.local/bin/elfdeckai-mcp"]}}}
This needs key-based SSH login from the PC to the Deck, because the MCP
client cannot answer a password prompt; AI_SETUP_GUIDE.md
covers the setup. Without pip, use
"command": "python3", "args": ["/path/to/elfdeckai_mcp.py"].
Every tool call is validated against the tool's schema and the daemon's own parser before anything is written to a FIFO. If a daemon is not running, the call returns an error immediately instead of blocking.
Command reference
ElfKey (/tmp/elfkey.fifo)
| Command | Effect |
|---|---|
key <name> [ms] |
Tap a key (default 120 ms) |
down <name> / up <name> |
Hold / release a key |
release |
Release every held key |
quit |
Release everything and exit |
Keys: a–z, 0–9, arrows, enter, esc, space, tab, backspace,
shift, rshift, ctrl, rctrl, alt, ralt, capslock, f1–f10,
home, end, pageup, pagedown, insert, delete and common
punctuation. The full table is KEYS in elfkey.py.
ElfPad (/tmp/elfpad.fifo)
| Command | Effect |
|---|---|
press <btn> [ms] |
Tap a button or D-pad direction (default 120 ms) |
down <btn> / up <btn> |
Hold / release |
stick left|right <x> <y> [ms] |
Set an analog stick, −100 to 100 per axis; with ms, recenter afterwards |
trigger left|right <0..100> [ms] |
Set an analog trigger; with ms, release afterwards |
center |
Return sticks, triggers and D-pad to neutral |
reset |
Release everything |
quit |
Release everything and exit |
Buttons: a b x y lb rb l2 r2 select start guide l3 r3, plus D-pad
directions up down left right.
ElfPad identifies as an Xbox 360 controller, so SDL, Steam Input and
RetroArch map it without configuration. l2 and r2 drive the analog
trigger axes, as on the real controller; there are intentionally no
digital trigger buttons, because declaring them shifts Select, Start and
Guide off the standard button indices.
ElfMouse (/tmp/elfmouse.fifo)
| Command | Effect |
|---|---|
move <dx> <dy> [glide_ms] |
Move the pointer relatively; with glide_ms, spread the move over that time |
moveto <x> <y> [<width> <height>] |
Place the pointer at an absolute position: pixel (x, y) of a width×height frame, or device units 0–32767 across the screen when no frame is given |
click <btn> [ms] |
Click left, right or middle (default 100 ms) |
down <btn> / up <btn> |
Hold / release a button (for drags: down, move, up) |
scroll <ticks> / hscroll <ticks> |
Vertical / horizontal wheel (positive is up / right) |
release |
Release every held button |
quit |
Release everything and exit |
move is relative: negative dy moves up. Its units are motion counts,
not guaranteed screen pixels, because pointer acceleration applies. Each
command is limited to ±10,000 counts and ±120 wheel ticks.
moveto uses a second virtual device, ElfMouse Absolute, which
compositors treat like a virtual machine's tablet pointer, so the position
is exact. Buttons are pressed on whichever device moved last.
Safety model
Because the caller may be a language model, the daemons are built so that bad input cannot leave the machine in a bad state:
- Malformed input is refused, not fatal. Every line passes a strict parser; invalid commands are logged and ignored.
- Taps are bounded. A timed hold lasts at most 5 seconds. Longer holds
use
down/up. - Nothing stays stuck. Timed actions restore in a
finallyblock, held inputs are tracked, and onquit, SIGTERM or SIGINT every input is released before the device is removed.key_release,mouse_releaseandpad_resetrelease everything on demand. - Owner-only access. Each FIFO is created with mode 0600.
- Audit log. Every accepted command is written to stderr (the journal, when run as a service).
- Robust protocol handling. The MCP server returns proper JSON-RPC errors for malformed or oversized requests and never blocks on a stopped daemon.
Scope: these are real OS-level input devices. They act on whichever window has focus, including terminals and browsers. ElfDeckAI keeps input bounded, validated and releasable; deciding where an agent is allowed to act is up to the operator. Only run agents you trust, in sessions you can supervise.
Comparison with other approaches
| Approach | Limitation |
|---|---|
| Emulator APIs (PyBoy, mGBA scripting) | Only work inside a specific emulator. |
| Sending keys through Moonlight or other streaming clients | Mouse clicks are forwarded, but synthetic keyboard events are not. |
| xdotool | Cannot reach native Wayland clients. |
| ydotool, dotool | Same uinput approach for keyboard and mouse, but no gamepad, raw keycodes, and no agent-facing interface (MCP, input bounds, release commands). |
| Hardware input emulators (KVM gadgets, controller adapters) | Work, but require extra hardware. |
Installation
Requirements: 64-bit Linux with /dev/uinput and Python 3.9 or newer.
Where /dev/uinput is not writable by your user, install the udev rule
once:
sudo cp 70-elfdeckai-uinput.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
getfacl /dev/uinput # your user should appear in the ACL
To run the daemons as systemd user services after pip install --user elfdeckai
(the unit files expect the commands in ~/.local/bin):
mkdir -p ~/.config/systemd/user
cp systemd/*.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now elfkey.service elfpad.service elfmouse.service
Development
pip install -e ".[test]"
python3 -m pytest
The test suite covers each daemon's input parser, device behaviour against
a fake uinput device, and the MCP server over real JSON-RPC. No
/dev/uinput is required to run it.
Roadmap
See ROADMAP.md.
Changelog
See CHANGELOG.md.
License
MIT. See LICENSE.
Author
Created and maintained by Garth Elfstrom (@Garthnesss). Issues and pull requests are welcome.
Metadata
Release files for elfdeckai 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| elfdeckai-1.1.0.tar.gz | 38.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| elfdeckai-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.0 kB
Release files / elfdeckai-1.1.0.tar.gz
| Download URL | elfdeckai-1.1.0.tar.gz |
|---|---|
| Size | 38.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
27503fd201d243ede568841d9fa596a519982f566030c9f0da71dc13221bb39c
|
|
BLAKE2b-256 checksum How to use checksums |
caf1568487f531420fc38a28aed1fb6504f143759ee7a74f7198704cebeb3fa2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logRelease files / elfdeckai-1.1.0-py3-none-any.whl
| Download URL | elfdeckai-1.1.0-py3-none-any.whl |
|---|---|
| Size | 31.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f9623dc59c05538c026529f235f3b021fe41ccb3d9517631973210938d02eec5
|
|
BLAKE2b-256 checksum How to use checksums |
db8e6fef2e2cb5b30e25bfe692ebbe165eaeebc36dc45c9fc2f3334cadbaaffe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logSecurity for elfdeckai 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for elfdeckai-1.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.