Imported from fracturedring/renpy-mcp (
AGENTS.md). Install upstream withnpx skills add fracturedring/renpy-mcp. Copyright stays with the author.
AGENTS.md
Short playbook for LLMs driving renpy-mcp (including low-tier models). If
you are an agent wired to this server, read this first — everything else in
the repo is reference material for humans.
The deal
- One MCP server per session. Your harness already speaks MCP; the tools
show up with the prefix your harness uses (
mcp_renpy_<name>for hermes-agent,mcp__renpy__<name>for Claude Code). - You author Ren'Py visual novels. You do not edit
.rpyfiles directly — every structural change goes through a tool. - Assets (images, audio) are generated by your host harness's own tools
(hermes ships a fal image tool; Claude Code's harness decides what it
has). Save those files into
<project>/game/images/or<project>/game/audio/, then register them through the MCP server. Before generating, callget_media_invariants— it returns the structured form of MEDIA.md (dimensions, alpha rules, audio formats) so you can pin the right shape on the first try and avoid a regenerate loop.add_image_aliaswill surfacemedia_warningsif the asset deviates anyway.
Default folder
By default games live at <cwd>/games/<slug>/. The server auto-scaffolds
a starter project at <cwd>/games/default/ if you never call
new_project. Prefer calling new_project with a slug derived from the
prompt so each conversation lands in its own directory.
Happy path from a one-sentence prompt
Given a prompt like "make a short VN about a lighthouse keeper meeting a selkie", drive the server in this order. Most calls are structured — pass arguments, don't concatenate strings.
new_projectwithnameset to a short title (e.g."Lighthouse Keeper"). The server scaffolds<cwd>/games/lighthouse_keeper/from the Ren'Py SDK template and rebinds the session. The response includes anext_stepsarray — read it; it covers the wiring step most agents forget (set_start_target).- Generate assets through your host harness. Call
get_media_invariantsfirst to confirm the dimensions / alpha you need; then for each background or sprite: call the harness's image tool (hermes: fal), save the PNG into<project>/game/images/<name>.png. For music: save into<project>/game/audio/<name>.ogg. - Register assets so Ren'Py knows about them:
add_image_aliasfor each background / sprite (e.g.name="bg lighthouse", asset="images/bg_lighthouse.png")- Music doesn't need registration —
play music "audio/..."refers to the file directly. If you want it per-scene, useset_scene_music.
- Define characters with
add_character— one call per speaker. - Build the story graph. Use the Tier 3 intents; they are the
right level of abstraction:
create_scene— a labeled scene with background, optional music, optional opening dialogue, optional terminator.create_choice_node— a label holding an optional prompt line then amenu:whose choices jump to target labels.create_route— chains a sequence of labels with jumps so you can sketch a route skeleton before filling each node.add_dialogue_block— append several say-statements to an existing label in one write.swap_background/add_character_to_scene/set_scene_music— fine adjustments to an existing scene.
- Wire the player entry. Brand-new projects ship with an empty
label start: return. Callset_start_target(target="<your opening label>")so clicking New Game lands the player on your scene. Skip this and the game ends the moment it begins. - Verify. Call
get_lint_report. If it returns errors, fix them and re-lint. Treat the lint as the source of truth — a game that lints clean will launch. - Preview. Call
launch_preview. It spawns the Ren'Py SDK against the bound project and returns immediately. Useget_preview_statusto check it;stop_previewto close.
Rules the server will hold you to
- Dialogue text is single-line. Do not put
\ninside atextfield — the server will reject it with a clear error. Split multi-line speech into separate calls (or separatelinesentries inadd_dialogue_block). - Labels are globally unique.
add_label/create_scene/create_choice_node/create_routeall refuse on collision. - Character vars must be Python identifiers and not reserved Ren'Py
keywords.
e,mei,sailor_01— fine.1sailor,class,menu— no. - Assets are validated.
add_image_aliasandadd_audio_playcheck the file exists undergame/unless you passvalidate_asset: false. Save the generated file before calling the register tool. - Metacharacters in dialogue are auto-escaped. Just type normal
English;
{,},[,]get doubled for you. Setraw: trueonly if you already escaped them yourself.
When things go wrong
- A tool returned
error. Read the message. Most errors point at the wrong argument — fix the call, don't retry identically. - Lint fails. Read the first failure in the report. Most come from
forward references (jump to a label you haven't made yet) or stray
indentation. The server keeps
.rpylint-clean for the pieces IT writes — if something's off it's almost always a forward-reference you can satisfy by creating the target label next. - Preview crashes. Check
get_preview_statusfor the last exit code, thenget_lint_reportfor the actual problem. The engine's own traceback lands in<project>/traceback.txt.
When the structured tools can't express something
Tier 4 (off by default) unlocks apply_unified_diff and
exec_python_in_init. These touch arbitrary file content. Only ask the
harness to enable Tier 4 when a Tier 2/3 tool genuinely cannot express
what you need — structured tools catch more mistakes.
Tool surface size for small models
If you're a low-tier model getting confused by 80 tools, ask the human
operator to start the server with --tiers 1,3. That keeps the reads
and the high-level intents (43 tools total) while hiding the 27 Tier 2
primitives that overlap with composers. The intents call the writer
pipeline directly so authoring still works end-to-end.
One-sentence-to-playable sketch
user: "make a short VN about a lighthouse keeper meeting a selkie"
agent:
new_project(name="Lighthouse Keeper")
get_media_invariants() # confirm 1920x1080 bg, 1080-tall sprite + alpha
# via host harness:
# fal_image(prompt="stormy night, lighthouse on cliff",
# width=1920, height=1080, save_as="...")
# fal_image(prompt="selkie woman in the surf, transparent bg",
# width=900, height=1080, save_as="...")
add_image_alias(name="bg lighthouse", asset="images/bg_lighthouse.png")
add_image_alias(name="selkie happy", asset="images/selkie_happy.png")
add_character(var="keeper", display_name="Ewan", color="#335577")
add_character(var="selkie", display_name="???", color="#88aacc")
create_scene(
name="opening",
background="bg lighthouse",
dialogue=[
{"character": "keeper", "text": "Another storm tonight."},
{"character": "keeper", "text": "Best keep the lamp lit."},
],
ends_with="jump",
jump_target="on_the_shore",
)
create_choice_node(
name="on_the_shore",
prompt={"character": "keeper", "text": "Something in the surf..."},
choices=[
{"text": "Approach carefully.", "target_label": "approach"},
{"text": "Stay back and watch.", "target_label": "watch"},
],
)
# ... fill in `approach` / `watch` with create_scene or add_dialogue_block
set_start_target(target="opening") # wire New Game -> opening
get_lint_report()
launch_preview()
That's the whole pattern. Start with new_project, wire start with
set_start_target once your opening label exists, end with
launch_preview, get_lint_report whenever in doubt.