Skip to main content
Glama
GrafxFrosch

steam-claude-controller

by GrafxFrosch

steam-claude-controller

An MCP server that lets Claude see and play Steam games on Windows: list and launch your Steam library, capture the game window, send timed keyboard/mouse input, and, for Source engine games like Portal, read exact game state and aim to the degree.

Tested end to end on Portal (AppID 400): launched into testchmb_a_00, read position/angles, turned to exact headings, and walked out of the Relaxation Vault through the portal.

Why not "the Steam Input API"?

Steam Input (ISteamInput) is the API a game uses to read controllers. Steam maps the physical pad to the game's actions (Portal ships steam_input/action_manifest_portal.vdf). No outside program can use it to press buttons in a game, and Steamworks only exists inside the game process. So the generic layer works one level lower:

Layer

What it does

Works for

steam.py

Reads libraryfolders.vdf / appmanifest_*.acf, launches via steam.exe -applaunch

every Steam game

win32.py / controller.py

Window capture (PrintWindow, screen fallback) + SendInput (VK + scancode, raw mouse deltas)

any game that accepts keyboard/mouse

source_engine.py

Console bridge, getpos state, exact aim, pause

Source 1 games (Portal, HL2, ...)

Engine adapters give Claude senses beyond pixels. Adding a game engine means adding one module like source_engine.py.

Related MCP server: steam-mcp

The Source console bridge

Portal 1 (Source 2013) has no remote console (-netconport doesn't exist there), so:

  1. source_launch writes portal/cfg/claude_bridge.cfg and launches with -condebug -windowed -noborder -w 1280 -h 720 +exec claude_bridge. The bridge binds INS to exec claude_cmd.

  2. To run a command, it writes claude_cmd.cfg (command + echo <nonce>), taps INS, and reads portal/console.log up to the nonce.

  3. If the bind isn't active (e.g. a menu has focus), it opens the console with `, types the exec line, and closes it again.

Aiming is exact because degrees = mouse_counts × sensitivity × m_yaw with raw input. In testing, a requested +90° came back as 90.04°.

Tools

Tool

Purpose

steam_list_games

Installed games, Source detection, anti-cheat flag

steam_launch

Launch any game (with args) and attach to its window

game_attach

Attach by appid / title / Steam's running app

game_screenshot

JPEG of the game's client area

game_act

Timed input sequence (key, down/up, move, click+at, text, scroll, wait), then a screenshot. freeze_after pauses Source games while Claude thinks

game_release_all

Release anything held

source_launch

Launch a Source game with the bridge, optionally into a map

source_console

Run console commands, get their output

source_status

Position, pitch/yaw, map

source_look

Relative turn by degrees, or absolute heading

source_pause

setpause / unpause

Setup

Requires Windows and Steam.

Claude Desktop (one-click bundle)

Download steam-controller-<version>.mcpb from the latest release and open it (or drag it into Settings → Extensions). The bundle uses the uv runtime, so Claude Desktop provides Python and installs the dependencies itself.

Claude Code (from source)

Requires uv.

git clone https://github.com/GrafxFrosch/steam-conroller-mcp.git
cd steam-conroller-mcp
uv sync

Claude Code sessions opened in the clone pick up the server from .mcp.json (approve it when prompted). To use it from any folder, register it at user scope with the claude CLI, replacing the path with your clone:

claude mcp add steam-controller --scope user -- uv run --directory "C:\path\to\steam-conroller-mcp" steam-claude-controller

Then ask, for example: "Launch Portal into testchmb_a_00 and get out of the vault."

Building the bundle

npx @anthropic-ai/mcpb@2.1.2 validate manifest.json
npx @anthropic-ai/mcpb@2.1.2 pack . dist/steam-controller-0.1.0.mcpb

.mcpbignore keeps .venv, .git and screenshots out of the bundle.

CLI for debugging

uv run scc ... mirrors the tools without an MCP client:

uv run scc games
uv run scc --appid 400 source-launch --map testchmb_a_00
uv run scc --appid 400 state
uv run scc --appid 400 look 90 0
uv run scc --appid 400 act "[{\"key\":\"w\",\"hold\":1.0}]" --shot shots/after.jpg

Safety and side effects

  • Focus guard: input is only sent while the game is the foreground window. If you click away mid-sequence, it releases all keys and stops, so nothing types into your apps. While Claude plays, the game holds keyboard focus.

  • Anti-cheat refusal: games with EasyAntiCheat/BattlEye/etc. folders are refused. VAC can't be detected from files, so don't point this at online multiplayer: synthetic input there can get the account banned.

  • Portal config: the bridge sets con_enable 1, m_rawinput 1, m_customaccel 0, m_filter 0 and binds INS. These persist in portal/cfg/config.cfg. console.log grows while -condebug is on. Delete claude_bridge.cfg / claude_cmd.cfg to remove the bridge files.

Known limits / next steps

  • D3D9 sometimes hands PrintWindow a black frame (e.g. during fades). Capture then falls back to a screen grab, which briefly focuses the game.

  • Walking into a portal off-center pins you against the wall beside it. Center on it first (source_status + source_look absolute).

  • Possible additions: a virtual gamepad via ViGEm (so Steam Input sees a real controller), a BepInEx bridge for Unity games (KSP, Polytopia), and Windows Graphics Capture for occluded D3D11/12 windows.

License

GPL-3.0, see LICENSE.

Related MCP Connectors

Related MCP Servers