Repository navigation
Add an MCP server for scripted in-game testing - #1025
Open
nicholasgower wants to merge 6 commits into
Open
nicholasgower wants to merge 6 commits into
nicholasgower wants to merge 6 commits into
Conversation
nicholasgower
force-pushed
the
feature/mcp-ingame-testing
branch
from
November 9, 2026 17:20
281ace4 to
94310fd
Compare
Collaborator
|
Hi! This PR has merge conflicts with |
Add scripts/hookshot_mcp.py, a stdio MCP server using the official Python SDK. It launches Hookshot, drives HOS_INPUT_LIVE, requests on-demand screenshots and returns them inline. Game-side seams: HOS_INPUT_SCRIPT_ONLY, analog stick tokens (LX/LY/RX/RY), !quit, HOOKSHOT_NO_ENV_FILE, RX_VK_SHOT_FILE and RX_SCREENSHOT_FILE. Tests: tests/test_mcp_server.py plus script-only and analog cases in the input fixture. Docs include MCP host configuration and the new switches. Assisted-by: DeepSeek V4.1 Flash Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
scripts/pr_check.py audits the branch before a pull request: game-derived paths, LF endings, Conventional Commit subjects and trailers, then the full game-free suite. It never stages, commits, pushes or calls GitHub. tests/test_pr_check.py builds throwaway repositories with a bad and a clean commit, and the docs tables and contributor checklist now mention the command. Assisted-by: DeepSeek V4.1 Flash Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
tests/test_shot_request.py extracts read_shot_request() from vk_replay.cpp and compiles a fixture for the missing, empty, trimmed, one-shot and buffer-edge cases behind RX_VK_SHOT_FILE. Assisted-by: DeepSeek V4.1 Flash Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
tests/test_docs.py resolves relative Markdown links, verifies documented python and powershell commands name tracked files, and compares docs/mcp.md's tool/resource tables with the live MCP server. Vendored third_party docs are skipped. Assisted-by: DeepSeek V4.1 Flash Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
A game folder made by HookshotBuilder or make_game.py keeps the game in bin\, with hookshot.exe at the top being the setup launcher that opens the installed builder (builder/setup/setup.c). --game-dir and HOOKSHOT_MCP_GAME_DIR chose the launcher, so launch started the builder setup instead of the game and no screenshot ever arrived. resolve_paths now prefers bin\hookshot.exe and falls back to the top of the folder for a development build, with a regression test for both layouts. docs/mcp.md gives the V2 OpenCode configuration (mcp.servers, no enabled field) and says that wait_ready returns on the first present, which on a cold cache is a boot frame while the shaders are still being prepared. Assisted-by: DeepSeek V4.1 Flash Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
scripts/hookshot_mcp.py gains settings(), set_setting() and cheat(), and launch takes cheats=true (HOS_OOT_CHEATS=1) so the cheat menu is on without editing hookshot_env.txt first. The game side answers three live input commands. !settings dumps every Insert menu row (its key, label, tab, value, range and the text the player sees) and, when the cheat menu is on, its eleven rows with the pouch's items, the teleport destinations and the inventory's items and counts. !setting <key> <value> goes through the menu's own setter, so it clamps, applies presets and control layouts and writes hookshot_options.ini, and a row that says it needs a restart still does. !cheat <name> <value> <extra> runs a cheat row the way Enter and the arrows do: the toggles, mark and return, the clock, teleports, the age, the pouch and the inventory. The rows cross the renderer table (settings_dump, setting_set and cheat in MkvVkRenderer, MKWVK_API_VERSION 8) from gpu/vk/vk_menu.cpp through host/rx_backend.c to host/nn_hid.c; the WebGPU back end has no Insert menu and says so. Cheat actions are now a queue rather than one atomic: two Enter presses in a frame, or an MCP cheat right after another, are both taken instead of the second overwriting the first (found by testing the tools against a running game). Tests: the fake game answers the three commands and the tools are exercised over MCP, tests/input_background_test.c covers the live command plumbing, and native_seams checks the new functions. docs/mcp.md documents the tools, the launch option and the commands. Assisted-by: DeepSeek V4.1 Flash Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
nicholasgower
force-pushed
the
feature/mcp-ingame-testing
branch
from
November 9, 2026 19:07
58da1b7 to
894961e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
scripts/hookshot_mcp.py, a Model Context Protocol server that turns a built Hookshot intosomething a test can drive. It is a separate Python process: nothing is linked into
hookshot.exe, and it needs no gamefiles of its own.
folder, save folder and shader/pipeline cache.
HOS_INPUT_LIVE) and wait for the game to logthat it read the line.
RX_VK_SHOT_FILE, WebGPURX_SCREENSHOT_FILE) and returns it inline as MCP image content plus a path on disk, with the present number.!quitlive command, then terminates if it does not.hookshot://log,hookshot://last-screenshot,hookshot://status.Game-side seams added for this:
HOS_INPUT_SCRIPT_ONLY, the analog stick tokensLX=,LY=,RX=,RY=, the!quitlive command,
HOOKSHOT_NO_ENV_FILE, and the one-shot screenshot request files.Docs:
docs/mcp.md(install, host configuration, tools, launch options, what the server sets, limits). The tool andresource tables in it are checked against the live server by
tests/test_docs.py.Why this matters for testing
Today the project has two testing speeds: game-free unit tests that run in seconds, and a person playing the game. Anything
in between — start the game, reach the title, open a menu, look at the frame — is done by hand, one run at a time, and
cannot be repeated after a change.
That is the gap this closes:
can be checked by the same scripted sequence: it either produces the expected frames or it does not. The log, the input
lines and the PNGs are left in a run folder to compare.
cutscenes, the pause and file menus, resolution and frame-generation settings, controller mappings and save paths
without a human at the keyboard.
same protocol; the tools are plain JSON calls and the frame comes back as an image, so "is the title screen drawn?" is a
question a model can actually answer about a real build.
this lands.
Rebased, and the previous PR
This is the same branch as #764, which was closed to keep the queue clear with "please rebase on current main and reopen".
It is now rebased onto
mainatb2765ba(v2.0.6 and the merges after it), and the game has been rebuilt from this branchand driven end to end.
What I tested
Not only the unit tests: the server was run against a real build of this branch, over its real stdio transport, and every
tool was exercised. Build: a normal builder layout (
hookshot.exelauncher,bin\,romfs), Vulkan back end, Windows,13 min 47 s, 0 failed modules, 100.0000% of 17,646,501 instructions translated.
Game-free suite (what CI runs):
tests/test_mcp_server.pygame_dir, fake-game end to endtests/test_docs.pytests/test_pr_check.pyscripts/pr_check.py --base mainThe full game-free suite gave 107 passed, 5 failed, 11 skipped. The five failures need tools this PC does not have — Vulkan
and SPIR-V headers, the Windows SDK headers for this compiler, and symlink privileges — and none of them touch a file in this
PR. The three new checks above pass.
Every tool against the real game, over stdio (34 checks):
launchwait_readyworksstatusVulkan (hookshot_vk.dll), uptime, last screenshot) and after stopscreenshotpresentparsed (e.g. "present 116"); the picture is the presented framepressAacknowledged in the game log, and it started New Game — the frame changed from the title screen to the intro cutscenestickLX=,LY=,RX=in the log; the frame changedsequence0:DOWN/120,400:DOWN/120,800:A/150); the frame changedwaitlogshid: live) and unfiltered; every injected line appearsstop!quit, exit code 0, process gone; a secondstopis safeextra_env, after stop — all clear messagesThe visual half of that run was checked too: title screen, then the cutscene after
A, then further changes afterstickandsequence— 12/12 visual checks. One detail worth knowing (now in the docs): the very firstwait_readyscreenshot of a coldrun is a black boot frame, because it returns on the first present while the shaders are still being prepared; the run above
waited for the title before asserting on the picture. The screenshots stayed on the development disk; no game-derived file is
in this PR.
Finally the server was installed as a project MCP server in OpenCode (V2
mcp.serversconfiguration) and the nine tools werecalled through OpenCode itself —
status,launch(withwait_ready),wait,press,stick,sequence,screenshot,logs,stop— all returned the expected results, and the run ended with exit code 0.Bug found and fixed by this testing
--game-dirandHOOKSHOT_MCP_GAME_DIRselected the folder'shookshot.exe. In a folder made by the builder ormake_game.pythat is the setup launcher, which opens the installed builder (builder/setup/setup.c), not the game inbin\;launchwould have started the builder setup and no screenshot would ever have arrived.resolve_pathsnow prefersbin\hookshot.exeand falls back to the top of the folder for a development build, andtests/test_mcp_server.pycoversboth layouts.
docs/mcp.mdalso now gives the V2 OpenCode configuration (mcp.servers, noenabledfield) and notes thatwait_readyreturns on the first present, which on a cold cache is a boot frame while the shaders are still being prepared.
On the CLI-first feedback in #764
The points raised there — a CLI, files instead of inline images, state in run folders, waits on game state, a GPU lock — are
reasonable, and this keeps the door open for them. Every tool is a thin call into
GameSession; the run state already livesin a run folder (
log.txt,input.txt,shot.request,shots/);screenshot(inline=false)returns a path instead of imagecontent; and input waits for the game's acknowledgement line rather than a fixed sleep. A CLI front end over the same
functions, a wait-on-log-state tool and a GPU lock are the natural next steps, and none of them need this PR to change shape.
Settings and the cheat menu (added since this PR opened)
settings()reads every row of the Insert menu — Graphics, Gameplay and Controls — with its key, label, value, rangeand the text the player sees. When the cheat menu is on it also returns the Cheats page's eleven rows, the pouch's
items, the 31 teleport destinations and the inventory's 85 items with their counts.
set_setting(name, value)goes through the menu's own setter: it clamps, applies a quality preset or a control layout,marks the settings Custom and writes
hookshot_options.ini. A row that says it needs a restart still does.cheat(name, value, item)runs a cheat row directly: heart protection, no-clip flight and the free camera; mark andreturn; the clock; teleports by name or index; the age; the pouch; the inventory (a wanted count, or -1 to
unlock/relock).
launch(cheats=true)turns the cheat menu on withHOS_OOT_CHEATS=1, which is otherwise a line inhookshot_env.txtbefore start.
On the game side these are three live-input commands —
!settings,!setting <key> <value>,!cheat <name> <value> <extra>— answered in the game log, which the server reads back. The rows cross the renderer table (API 8) fromgpu/vk/vk_menu.cppthroughhost/rx_backend.ctohost/nn_hid.c. The WebGPU back end has no Insert menu and says so.Testing found and fixed one real defect: two cheat actions in quick succession overwrote each other (the menu kept a single
atomic), so an inventory edit sent right after a pouch edit was lost. They are a queue now, and both are taken.
Real-game verification against a build of this branch (Vulkan, a copied save): 23 settings listed with ranges and text;
set_setting("frame_pacing", 1),set_setting("text_speed", 3)andset_setting("frame_generation", 0)applied, shown bysettings()and written tohookshot_options.ini; an unknown key refused; heart protection and the free camera toggledand read back;
cheat("set_time", 18)made the game log "cheats: requested 18:00";cheat("pouch", item="Bombs", value=5)logged "cheats: pouch Item_Bomb +5";
cheat("teleport", item="Kakariko Village")moved Link from Death Mountain Trail toKakariko Village (screenshot, and "cheats: teleport child to KakarikoVillage_Child ... request sent");
mark_positionlogged the position; two actions sent back to back both ran.
The game-free suite: 134 passed, 11 failed, 14 skipped — the same eleven environment failures as
origin/mainin aclean worktree.
Not included
No game files, dumps, builds, saves, logs or screenshots. Test runs stayed under
work/, which is ignored.