---
title: "Drive a running game over MCP"
url: https://recursiveai.org/onegame/cookbook/live-control-mcp/
description: "Control a running Debug build of a C# game from an MCP client or a script: read state, click by id, step time, send commands, load fixtures and capture…"
version: "1.2.0"
---

# Drive a running game over MCP

Control a running Debug build of a C# game from an MCP client or a script: read state, click by id, step time, send commands, load fixtures and capture frames over the onegame live-control protocol.

Debug builds of a game listen for live control on a local socket. `onegame mcp` bridges that socket to an MCP server on stdio, so any MCP client, or a script that speaks JSON-RPC, can drive the game with the same operations scenarios use. Release builds and exports contain none of this.

## From an MCP client

Point the client at the bridge. With `--project` it builds the game, starts it in a window and bridges to it:

```json
{ "mcpServers": { "orchard": { "command": "onegame", "args": ["mcp", "--project", "."] } } }
```

`--socket <path>` attaches to a game that is already running instead. With neither, the bridge attaches to the only live game.

## From a script

Start the game headless with a live socket. It waits for `step` instead of running on the clock (add `--realtime` to let it run):

```shell
onegame run --headless -- --live /tmp/onegame/orchard.sock
```

```text
listening on /tmp/onegame/orchard.sock (MCP, newline-delimited JSON-RPC); connect with: onegame mcp --socket /tmp/onegame/orchard.sock
```

A small MCP client in Python, for the apple tree from [Getting started](https://recursiveai.org/onegame/docs/getting-started/):

```python
import json, subprocess, sys

# Start the MCP bridge with this script's arguments (here --socket <path>); it speaks JSON-RPC on stdio.
bridge = subprocess.Popen(["onegame", "mcp", *sys.argv[1:]], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
next_id = 0

def rpc(method, params=None):
    global next_id
    next_id += 1
    bridge.stdin.write(json.dumps({"jsonrpc": "2.0", "id": next_id, "method": method, "params": params or {}}) + "\n")
    bridge.stdin.flush()
    return json.loads(bridge.stdout.readline())

def call(name, args=None):
    result = rpc("tools/call", {"name": name, "arguments": args or {}})
    print(f"{name} {json.dumps(args or {})} -> {result['result']['content'][0]['text'][:200]}")

rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "probe", "version": "1"}})
bridge.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
bridge.stdin.flush()
tools = rpc("tools/list")["result"]["tools"]
print(len(tools), "tools")

call("info")
call("step", {"frames": 300})
call("state_query", {"entity": "Tree"})
call("ui_click", {"id": "pick"})
call("step", {"frames": 1})
call("state_query", {"entity": "Tree"})
call("capture_frame", {"path": "shots/live.png", "size": "360p"})
call("quit")
```

```shell
python3 mcp_probe.py --socket /tmp/onegame/orchard.sock
```

```text
51 tools
info {} -> {"game":"orchard","tick":0,"frame":0,"hash":"6554bfe7b94da5ca","width":1280,"height":720,"scale":1,"headless":true,"screen":"main","entities":1,"screens":["main"],"fixtures":["heavy-tree","bare-tree"]
step {"frames": 300} -> {"frame":300,"tick":300,"hash":"d0045829889cf126"}
state_query {"entity": "Tree"} -> {"id":1,"type":"Tree","Apples":13,"Picked":0,"Growth":0}
ui_click {"id": "pick"} -> {"focused":"Button#pick","hovered":"Label","tick":300}
step {"frames": 1} -> {"frame":301,"tick":301,"hash":"80894b787970351f"}
state_query {"entity": "Tree"} -> {"id":1,"type":"Tree","Apples":12,"Picked":1,"Growth":1}
capture_frame {"path": "shots/live.png", "size": "360p"} -> {"path":"/tmp/onegame/orchard/artifacts/shots/live.png","width":640,"height":360,"scale":1,"sha256":"c944c46957490070d825687d2346bb511c55d0ae15387a4938a5bd100dc03e16","renderer":"NVIDIA GeForce RTX 40
quit {} -> {"frame":301,"tick":301,"hash":"80894b787970351f"}
```

The script cuts each result at 200 characters. The hash after 300 steps, `d0045829889cf126`, is the one `onegame run --headless --frames 300 --json` prints: a live session is the same deterministic game.

## The tools

The 51 tools at 1.2.0:

| Area | Tools |
|---|---|
| Read | `info`, `hash`, `state_query`, `commands_list`, `ui_tree`, `ui_query`, `ui_hit`, `net_status`, `actions` |
| Change state | `command_send`, `state_patch`, `step`, `reset`, `fixture_load`, `snapshot_load`, `screen_open` |
| Input | `ui_click`, `ui_tap`, `ui_invoke`, `input_pointer`, `input_key`, `input_text`, `input_composition`, `input_nav`, `input_pad`, `input_touch`, `clipboard_get`, `clipboard_set` |
| Output | `capture_frame`, `capture_sequence`, `snapshot_save`, `audio_capture` |
| Checks | `theme_lint`, `audio`, `audio_events`, `shaders`, `render_graph`, `assets`, `draw_view` |
| Network | `net_wait`, `net_diverge` (corrupts a replica on purpose, to prove drift detection) |
| 3D | `camera`, `pick`, `project` |
| Platform | `ui_scale`, `quality`, `app_lifecycle`, `platform_native`, `store`, `steam_input` |
| Session | `quit` |

New in 1.2.0: `actions` reports the input actions and presses one by name, `input_pad` holds gamepad buttons and sticks, `assets` lists loaded files and reloads them, and `draw_view` reports what the 2D views drew. See [Input actions](https://recursiveai.org/onegame/cookbook/input-actions/) and [Draw sprites, shapes and text](https://recursiveai.org/onegame/cookbook/draw-sprites-2d/).

Errors come back as `{"error":{"code","message"}}` with a stable code such as `bad_args`, `not_found` or `path_outside`. File paths are relative to the game’s output folder (`artifacts/` by default).

## Good to know

- `--live` needs a Debug build: a Release build exits 5 with `no_dev_tools`.
- A windowed game can be driven the same way while it presents (`onegame run -- --live <socket> --realtime`). `capture_frame` with `"presented": true` returns the frame the window shows.
- Each node of a cluster has its own socket. `onegame cluster call <node> <tool> <json>` runs one tool, and `onegame mcp --cluster .onegame-cluster` serves every node, with a `node` argument on each tool. See [Client/server sync with a dedicated server](https://recursiveai.org/onegame/cookbook/client-server-sync/).
- The socket is local. Live control over the network (`--live tcp:*:PORT`) is for devices on a network you trust.

More in [Driving a game](https://recursiveai.org/onegame/docs/driving-a-game/), sections 5 and 6.
