llm-game-automation-mcp
Health Warn
- License — License: NOASSERTION
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 7 GitHub stars
Code Fail
- exec() — Shell command execution in mcp/src/agentloops.ts
- exec() — Shell command execution in mcp/src/camerahelpers.ts
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
MCP server + library that lets LLM agents play Windows games in the background: per-process injected input and frame capture, no window focus needed. Offline/single-player games only.
LLM Game Automation MCP
[!WARNING]
Single-player / offline games only. Do not use this in online or multiplayer games, or with any game
protected by anti-cheat: you will very likely be banned. It injects a DLL into the target game process.
vinput refuses to inject when it detects a known anti-cheat, but that check is a courtesy, not a guarantee.
Use at your own risk: the authors are not responsible for bans, lost saves, data loss, damage or anything else
that results from using it. Read DISCLAIMER.md first.
Let an LLM agent (or a script) play Windows games in the background. Each game process gets its own private
keyboard, mouse, gamepad and eyes:
- vinput - a DLL injected into one game process plus a CLI (
vinput.exe) that talks to it over a per-process
named pipe. Input is delivered inside that process on every channel games read; frames are captured from the
game's own swapchain. - MCP server (
mcp/) - wraps the CLI as Model Context Protocol tools
(launch,capture,click_text,key,pad, ...), so Claude or any MCP client can look at a game and act on it.
Why it is different
Most "AI plays games" setups drive the real desktop: SendInput plus a screenshot of the whole screen. That needs the
game in front, steals your mouse and keyboard, and allows exactly one game at a time. vinput works per process:
- The game never needs focus. It keeps running and rendering in the background; vinput fakes focus inside the game
and swallows its deactivation messages. - You keep your PC. vinput never generates OS-level input (no
SendInput); your real keyboard and mouse are hidden
from a game that is not really in front, the game cannot steal the foreground from you, and its own cursor moves
and clips go to a virtual cursor instead of your real one (see Cursor guard and physical input). - Several games at once. Every game has its own pipe and command queue, so several agents can drive several
games in parallel, each with an optional ownership claim. - Pixels from the game itself. Captures come from the frame the game presents (not a desktop screenshot), so they
work while the window is covered by other windows.
Supported APIs
| supported | notes | |
|---|---|---|
| Capture | Direct3D 11, Direct3D 12, OpenGL, Vulkan, DirectDraw (virtual primary surface), PrintWindow fallback |
HDR formats (R10G10B10A2, RGBA16F) converted to 8-bit; PNG or JPEG, optionally scaled inside the game; contact sheets (capture-burst); Vulkan needs early injection (launch --early); D3D9/D3D10 fall back to PrintWindow |
| Keyboard | window messages (WM_KEYDOWN/WM_CHAR, with the game's own TranslateMessage), Raw Input (WM_INPUT, GetRawInputBuffer on x64), DirectInput 8 and legacy dinput.dll (DirectInput 1-7), GetAsyncKeyState/GetKeyState/GetKeyboardState |
mirrors the game's own RegisterRawInputDevices choices (e.g. RIDEV_NOLEGACY) |
| Mouse | messages, Raw Input, DirectInput 8, virtual cursor behind GetCursorPos / WindowFromPoint |
absolute (click, move) and relative (move_relative, mouse-look) |
| Gamepad | XInput pad 0 (XInputGetState, XInputGetStateEx) |
buttons, both sticks, triggers; timed ramps and state timelines for proportional steering |
| Vision (CLI side) | Windows OCR (Windows.Media.Ocr), normalised cross-correlation template matching |
runs in the CLI, nothing extra is loaded into the game |
| Targets | 64-bit and 32-bit games, UWP / Xbox-app (AppContainer) games | vinput.exe hands 32-bit targets to vinput32.exe |
Install (prebuilt, no compiler needed)
Windows only. You need Node.js 20 or newer.
Download
llm-game-automation-mcp-win-x64.zipfrom the
latest release and unzip it, for
example toC:\Tools\llm-game-automation-mcp.Add it to your MCP client. Claude Code:
claude mcp add game-automation -- node C:\Tools\llm-game-automation-mcp\mcp\build\index.jsCodex (
~/.codex/config.toml):[mcp_servers.game-automation] command = "node" args = ['C:\Tools\llm-game-automation-mcp\mcp\build\index.js'] startup_timeout_sec = 60Other clients: see MCP quick start.
Start a new chat and ask the agent to launch a single-player game and play it in the background.
The zip contains vinput.exe, vinput32.exe, vinput64.dll, vinput32.dll and the built MCP server with its
dependencies, so there is no npm install step. The binaries are not code-signed, and they inject a DLL into a game,
so antivirus software may flag them. To build everything yourself instead, see Build.
Requirements
- Windows 10 or 11, x64.
- To build: Visual Studio 2022 or newer (or the Build Tools) with the Desktop development with C++ workload (x64
and x86 compilers). - For the MCP server: Node.js 20 or newer.
- The game should run windowed or borderless. Exclusive fullscreen loses its swapchain as soon as another window is
in front.
Build
build.bat
build.bat finds MSVC through vswhere; set VINPUT_VCVARS_DIR to a ...\VC\Auxiliary\Build folder to pick a
specific install. It builds x64 and x86 into dist\ (git-ignored): vinput.exe, vinput32.exe, vinput64.dll,vinput32.dll, vinput_x64.lib, vinput_x86.lib and the synthetic test targets. MinHook and the Vulkan headers are
vendored in third_party\, so nothing else is downloaded.
MCP server:
cd mcp
npm ci
npm run build
npm test
npm test is the MCP handshake test (tools/list against the built server); it needs no game. Self-tests: see
TESTING.md.
CLI quick start
rem start a game and inject (prints {"ok":true,"pid":1234,...})
dist\vinput.exe launch --exe "C:\Games\MyGame\MyGame.exe" --args "-windowed"
rem or inject into a game that is already running
dist\vinput.exe inject --pid 1234
dist\vinput.exe --pid 1234 capture shot.png
dist\vinput.exe --pid 1234 click-text "New Game"
dist\vinput.exe --pid 1234 wait-text "Choose difficulty"
dist\vinput.exe --pid 1234 key enter
dist\vinput.exe --pid 1234 hold w 1500
dist\vinput.exe --pid 1234 click left 480 300
dist\vinput.exe --pid 1234 pad a
dist\vinput.exe --pid 1234 status
capture saves the game's own frame; click-text / wait-text act on and verify on-screen text (OCR); hold w 1500
holds W for 1.5 s; coordinates are client pixels, which equal capture pixels; pad drives XInput pad 0; status
shows, among other things, which input APIs the game really reads.
Every command prints one JSON line; exit code 0 means "ok":true. ok:true on an input command means the input was
delivered, not that the game reacted: follow it with wait-text, wait-change or a capture. Full reference:
docs/CLI.md. Practical advice for agents that drive games (windowed sizes, click timing, drag-and-drop,
per-game notes): docs/AGENT_TIPS.md.
Readiness, timed input and safety controls
rem read-only: are the hooks, the window and its message queue ready? (inject runs the same check)
dist\vinput.exe --pid 1234 readiness --wait-ms 5000
rem a keyboard sequence file (hold w+space 1500 / wait 100 / key d 300) runs as ONE background job
dist\vinput.exe --pid 1234 seq flight.vin
dist\vinput.exe --pid 1234 job-status JOB_ID
rem gamepad: A plus a fully pressed left trigger for 300 ms; a timeline with staggered presses
dist\vinput.exe --pid 1234 pad a+lt 300
dist\vinput.exe --pid 1234 pad-timeline states.json --stagger-ms 60 --strict-frames
rem let your physical keyboard / mouse reach the game for one second, then vinput restores isolation by itself
dist\vinput.exe --pid 1234 config isolate=false --for 1000
rem click on coordinates read off a screenshot (the map is bound to that window and client size)
dist\vinput.exe --pid 1234 capture shot.png --max-width 960
dist\vinput.exe --pid 1234 click left 120 80 --capture-space
- Input readiness.
injectwaits (up to five seconds) for the keyboard / focus hooks, an adopted window and a
message queue that is making progress, and reports areadinessobject (hook_ready,window_ready,queue_ready,readiness_version, and the reason for a failure).readiness [--wait-ms 0..10000]repeats the check without
injecting anything;inject --hooks-onlychecks only the hooks (suspended or window-less setup, whichlaunch --early
uses).application_readyis alwaysnull: vinput cannot know that a game or a program inside it has finished
starting, so verify the first action on screen.pingkeeps its meaning (the pipe is alive). - Async key sequences.
seq FILE(aliassequence) validates the whole file first, then returns a background job
ID at once. Lines arehold CHORD MS,key CHORD MS,wait MSorsleep MS;#starts a comment. Cancelling
(job-cancel) releases the active chord; the keys of later steps are reserved, so pressing one by hand cancels the
rest. At most 32768 bytes, 1024 steps and 118000 ms of declared timing; the 120-second job lifetime cap still applies. - Gamepad.
ltandrtare button-list aliases for the triggers (pad a+lt 300presses LT fully; a partial
trigger ispad a 300 --lt 128); an alias plus an explicit value for the same trigger is rejected. Inpad-timelineevery state is a complete snapshot (omitted axes are 0) and may carrybuttonsand aphaselabel.--stagger-ms 0..1000holds back a newly pressed button or trigger until the movement stick has been published first
(and, with native frame pacing, a later presented frame); releases are never delayed. Receipts report what vinput
published, not that the game consumed it. - Capture diagnostics. A
PrintWindowcapture reply carriescapture_diagnostics: the window the frame came from,
its class, caption and client size, a bounded list of other windows in the same process, and whether the full client
image is black or one flat colour. A black image next to a second same-process window is an observation, not proof
that the other window is the renderer: vinput does not switch its capture or input target by itself (tryconfig hwnd=0x...yourself). - Screenshot coordinates.
--capture-spacecoordinates belong to one adopted window and one client geometry. After
a resize or a retarget the map is refused: take a fresh single-framecapture(hashes andcapture-burstdo not
refresh it). A DLL withoutcapture_scope_version:1is refused for these coordinates, and a positioned mouse-up
still attempts a coordinate-free release. A resize that happens and reverts without vinput seeing an event cannot be
detected. - Window binding and retargets. Each adopted window keeps its own original window procedure and its A/W character
encoding, so retargeting between windows does not cross their hook chains. If another payload has left an opaque
hook chain on an old window,config hwnd=...refuses to re-adopt it (adoption_refused:truewith a reason) and
keeps the current target. A target change is also refused while held virtual input, queued legacy messages or an
owned character translation still need the old window (status.retarget_quiescence): runrelease,readiness --wait-ms 2000, and retry. A refusal changes no setting and no job.
Cursor guard and physical input
While vinput is injected and its hooks are armed, SetCursorPos, SetPhysicalCursorPos, ClipCursor, SendInput,mouse_event, GetCursorPos, GetPhysicalCursorPos and GetClipCursor inside the game go to a virtual cursor,
not your real one:
- A cursor-position write or injected input from the game reaches the real desktop only when the game owns the real
foreground (checked through two independent queries) and either fresh physical input arrived in its window
within the last 300 ms or a foreground lease is active andcursor=virtualis not forced (vinput --pid N foreground [--lease-ms 30000], a hand-off
and verification command that really takes the foreground). Otherwise the call is virtualised or refused and counted. ClipCursoris real only while you play the game: a clip the game sets is forwarded to Windows only when the
game's adopted window owns the verified real foreground and the rectangle lies inside its client area and the
desktop. In the background it stays virtual. vinput releases a clip it forwarded when the game loses the
foreground, its window changes or its size changes (best-effort: up to 3 attempts, and only if the clip is still the
one it set).GetClipCursorreports the game's requested clip, andstatus.cursor_guard.native_clipshows what was
forwarded and released.keybd_eventis not hooked, and a program that talks to the kernel directly bypasses every user-mode hook. The
counters instatus.cursor_guardare scoped to the hooked entry points;real_cursor_writes_leakedisnull
because it is not measured. Input commands refuse a DLL that lacks the cursor guard or whose hooks are not armed.
While isolate is on (the default), the keyboard, mouse, raw-input and DirectInput readers return nothing from your
physical devices unless this process owns the verified real foreground. Virtual input and raw packets the game
itself posts are separate. config isolate=false passes physical keyboard and mouse input through even in the
background and stops hiding the physical gamepad while the game is in the background; status shows aphysical_input.warning for as long as it is set. Prefer the bounded form, config isolate=false --for 1000
(1..120000 ms): the deadline lives inside the game, is checked by every reader, status and the monitor thread,
and restores isolate=true even if the CLI or this MCP server goes away. A later config isolate=true cancels it, anisolate=false without --for is indefinite. Cursor-write safety does not depend on isolate.
Known limits of this contract:
- A game that sets its own cursor position while really in front of you is virtualised unless you just moved
the mouse over it or took a foreground lease; test such a game while it is in front withvinput --pid N foreground --lease-ms 30000first. (ClipCursorfollows the foreground rule above.) - A program running inside the game (a trainer, for example) that calls target-side
SendInputormouse_event
produces global OS input when it is forwarded; fake focus does not redirect it into the game. The guard forwards it
only under the rule above (verified real foreground plus fresh human activity or a lease), and a forward reports an
attempt, not that the application accepted it. - If the game (or another hook) removes vinput's marker from its window, the window keeps working (its own window
procedure still gets every message), but vinput refuses input to that window from then on andstatus.wndproc_bindingreports it as quarantined. Restart the game or select a fresh window; recovery on the same
window is not supported. - In-process raw packets (a program inside the game posting its own
WM_INPUT) are delivered through a private
envelope that is unwrapped insideDispatchMessageA/W; games that readmsg.message == WM_INPUTfrom their own
message loop without dispatching, filter onWM_INPUT, or consume raw input from another queue do not see them.
Packets can be served to a game that still ignores them: that downstream case is not fixed, only measured on
synthetic targets. - Isolated legacy character translation needs Windows 10 1607 or newer and an ordinary
TranslateMessagethenDispatchMessageA/Wpump; dead keys, IME and Alt+numpad are not guaranteed (usetypefor exact text). - While isolating, keyboard window messages that vinput did not publish (IME, an on-screen keyboard, the game posting
to itself) are dropped in the game's window procedure, and pointer / touch window messages are not filtered. - Cursor protection, physical-input isolation and retarget quiescence are tested on synthetic targets only; the
behaviour of a particular commercial game, and of a real human using the same window at the same time, is not
measured. See TESTING.md.
MCP quick start
Build the server (above), then add it to your MCP client. Claude Code:
claude mcp add game-automation -- node C:\path\to\llm-game-automation-mcp\mcp\build\index.js
Claude Desktop (claude_desktop_config.json) or any stdio MCP client:
{
"mcpServers": {
"game-automation": {
"command": "node",
"args": ["C:\\path\\to\\llm-game-automation-mcp\\mcp\\build\\index.js"]
}
}
}
Then ask the agent to launch a game and play it. The tools, their schemas and a short agent quick start are in
mcp/README.md.
How it works (inside the game process)
| part | how |
|---|---|
| fake focus | GetForegroundWindow / GetActiveWindow / GetFocus report the game; the game's window procedure is subclassed and deactivation (WM_ACTIVATEAPP(0), WM_ACTIVATE(WA_INACTIVE), WM_NCACTIVATE(0), WM_KILLFOCUS) is swallowed, so it keeps running and rendering in the background |
| no focus stealing | while the game is not really in front, SetForegroundWindow / BringWindowToTop / SetActiveWindow are no-ops, SetWindowPos gets SWP_NOACTIVATE, showing the window does not activate it, cursor writes (SetCursorPos, ClipCursor, SendInput, mouse_event) go to a virtual cursor unless you are really using the game's window (cursor guard, below), DXGI / DirectDraw / D3D9 exclusive fullscreen becomes a windowed virtual fullscreen (or is refused, config fullscreen=refuse); every attempt is logged |
| virtual input | each event is delivered on every channel the game can read, as a real key would be (see Supported APIs) |
| isolation | while the game does not own the verified real foreground, the physical keyboard / mouse / raw-input / DirectInput readings are hidden from it, so your typing elsewhere does not leak into it; when it is in front, real input works and virtual input is added on top (and the game's own ClipCursor takes effect); config isolate=false --for MS is a bounded opt-out |
| eyes | native capture from the game's swapchain (per API), with PrintWindow as the fallback; replies report capture_path and the frame number |
| control | named pipe \\.\pipe\vinput_<pid>, one JSON object per line; vinput.exe wraps it |
| anti-cheat refusal | src/vi_acguard.h: the CLI refuses to inject, and the DLL refuses to start or keep working, when a known anti-cheat is detected (see below) |
Anti-cheat refusal
Before it injects anything, vinput refuses - with {"ok":false,"anticheat_refused":true,...}, exit code 4 and
no override flag - when:
- the target process has a module loaded whose name belongs to a known anti-cheat;
- the game's folder (or up to three parent folders) ships a known anti-cheat component, e.g. an
EasyAntiCheatorBattlEyefolder orstart_protected_game.exe(this catches a protected game before its anti-cheat loads, and
is checked beforelaunch --exestarts anything); - a known anti-cheat process is running, or a known anti-cheat service or kernel driver is active, anywhere on the
machine.
The injected DLL repeats check 1 when it starts, and watches module loads: if an anti-cheat module is loaded into the
game later, every input and capture command is refused from then on.
Covered by name: EasyAntiCheat, BattlEye, Riot Vanguard (vgc / vgk), Ricochet, FACEIT, ESEA, nProtect GameGuard,
XIGNCODE3, ACE (AntiCheatExpert), PunkBuster, mhyprot, Denuvo Anti-Cheat. The lists are in src/vi_acguard.h.
This is defence in depth, not a guarantee. It is name-based: a renamed, new or unlisted anti-cheat is not
detected, and an absent refusal does not mean a game is safe to automate. Because check 3 is machine-wide, a kernel
anti-cheat that stays resident (for example Vanguard's vgc service) blocks vinput until it is stopped.
Limitations
- Exclusive fullscreen is not supported: vinput turns DXGI, DirectDraw and D3D9 fullscreen requests into a windowed
virtual fullscreen, but a game's own saved fullscreen mode is best switched to windowed / borderless in its settings.
A minimised game stops rendering (vinput puts a background game back unlessconfig keepvisible=false). - Inputs vinput does not reach: direct syscall /
NtUserGetAsyncKeyStatereaders; Windows.Gaming.Input,
GameInput and WinRTCoreWindowkeyboard events (many UWP games: usepad); HID read directly; DirectInput
joysticks; Steam Input;GetRawInputBufferin 32-bit processes; XInput users 1-3. - Capture: Vulkan only when vinput is injected before the game creates its device (
launch --early); D3D9 and
D3D10 are not captured natively;PrintWindowcan be black or white for some surfaces. - OCR has no confidence score and can misread stylised or very small text: use
--region,--scale 2or image
matching. Image matching does not handle rotation and finds scales within about +-20 % by default. - Text:
typepostsWM_CHAR(right for text boxes); games that only read raw keys needtype --keys, which does
not apply Shift for punctuation yet. - The game can still be brought to the front by other programs; while it is really in front it receives the real
keyboard and mouse, and a cursor clip it sets applies only while it stays in front (released when it loses it). - vinput stays loaded in a game until the game exits.
- Electron / Chromium games (for example Phantom Rose) slow down or freeze their rendering when Chromium thinks
the window is hidden. Launch them with Chromium's anti-throttling switches:args"--disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows --disable-features=CalculateNativeWinOcclusion". - Verified live on a set of commercial D3D11, D3D12 (UWP), OpenGL and Vulkan (with
launch --early) games. See
TESTING.md.
Repository layout
src/ vinput DLL (vinput_core.cpp; capture: vi_cap12.h vi_capgl.h vi_capvk.h; cursor guard, isolation, readiness,
posted input: vi_cursor_*.inl vi_isolation_lease.inl vi_readiness.inl vi_posted_input.inl ...), CLI (vinput_cli.cpp, vi_cli_*.inl),
anti-cheat refusal (vi_acguard.h), OCR / text / image matching (vi_ocr.h vi_text.h vi_ncc.h vi_vision.h)
test/ synthetic targets: input logger, D3D12 / GL / Vulkan / XInput targets, UI menu target, anti-cheat refusal target
verify/ PowerShell self-tests (run_selftest.ps1 runs the suite)
macros/ example .vin scripts for the synthetic UI target
mcp/ the MCP server (TypeScript)
third_party/ MinHook, Khronos Vulkan headers (with their licences)
docs/CLI.md full CLI reference
docs/AGENT_TIPS.md tips for LLM agents (windowed setup, click timing, drag, game notes)
docs/MCP_TODO.md MCP follow-ups that wait for core features
Compiling vinput into your own DLL
Instead of injecting vinput64.dll, you can link dist\vinput_x64.lib (or vinput_x86.lib) into a DLL of your own:
compile with /DVINPUT_COMPILE_IN /I<repo>\src, link d3d11.lib dxgi.lib dxguid.lib windowscodecs.lib ole32.lib,#include "vinput.h" and call VINPUT_INIT(); from your init (safe in DllMain; it only starts a thread). Without
the define VINPUT_INIT() compiles to nothing. vinput's MinHook is renamed (VIMH_*), so it links next to your own
MinHook, and both hook chains work. If vinput64.dll is also injected, the second copy is a no-op. The anti-cheat
start-up check runs in this mode too.
Contributing, security, licence
- CONTRIBUTING.md - PRs welcome; every change comes with a red/green test; offline-only scope.
- SECURITY.md - how to report a vulnerability.
- MIT licence, copyright CorruptedAngel - see LICENSE. Third-party components: THIRD_PARTY_NOTICES.md.
- Disclaimer: DISCLAIMER.md.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found