tmodloader-mcp

io.github.musharna由 GitHub 帳號 musharna 發布v0.6.3更新於 Oct 27, 2026

Drive a running tModLoader (Terraria) instance from an agent: launch, query, screenshot, read state.

概覽

AI 產生的概覽

讓助理驅動執行中的 tModLoader(泰拉瑞亞):啟動與停止工作階段、查詢遊戲狀態、截圖並讀取日誌。

可以做什麼
  • 啟動、加入、重新啟動、停止並檢視遊戲工作階段,列出世界、角色與模組
  • 向遊戲送出觸發指令,讀取診斷資訊、圖格數量、實體、玩家與截圖
  • 依區域截圖,並列出、讀取或清理已保存的截圖
  • 讀取、篩選並監看遊戲日誌,包括已輪替的舊執行紀錄
  • 建置模組、檢索已安裝的 tModLoader API,並快照或還原存檔
  • 執行內建提示,診斷模組沒有回應並列出實際存在的世界與角色
適用情境
適合讓助理在測試或除錯 tModLoader 模組時看到真實遊戲狀態,而不是憑空猜測,例如改動後檢查生成物、圖格數量、實體位置或截圖。它處於 alpha 階段,且只在一套安裝上跑過,宜視為實驗性工具。
執行需求
需要在 Windows 上以 WSL2 驅動 Windows 版 tModLoader(不支援原生 Linux 或 macOS);Python 3.12 以上;已安裝 tModLoader(測試於 1.4.4.9);需把 C# 回應器資料夾複製進自己的模組並繼承對應類別;只有 API 檢索需要 .NET SDK。必需環境變數 TMODLOADER_SAVE_DIR 與 TMODLOADER_MOD_SOURCE,選用 TMODLOADER_DIR、TMODLOADER_WORLD_WIN、TMODLOADER_MOD_SOURCE_WIN、TMODLOADER_MOD_NAME、TMODLOADER_TASKLIST、TMODLOADER_TASKKILL 與 TMODLOADER_POWERSHELL。
安裝前請注意
選用的寫入指令會改動世界:time、weather、spawn、give、teleport、settile、cleartile 與 despawn,還能執行模組自帶指令與聊天。停止工作階段是強制結束且不存檔,但執行較久或正常結束會寫入磁碟,改動重要世界前應先做存檔快照。截圖可能包含角色名稱、世界名稱與畫面上的聊天內容。它會呼叫 Windows 的 tasklist 與 taskkill 程序,並需要存檔與模組原始碼路徑。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 tmodloader-mcp,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

[a pixel-art blue slime]

tmodloader-mcp

Drive a running tModLoader instance from an agent — launch it, ask it questions, photograph it, and read its state back as structured data.

[CI] [PyPI] [Python 3.12 | 3.13] [MIT]

How it works · Quick start · Configuration · Known limits

Game engines have grown MCP servers — Unity, Unreal, Godot and Defold all have one, so an assistant can see a real scene instead of guessing from a prompt. tModLoader has not had one. This is that.

[A running game at noon: the player at spawn among blue slimes, forest, HUD]

The tool staged its own screenshot — time:noon, teleport:spawn, spawn:1,25, shot:full — read from the game's back buffer, so a window in front of the game cannot be in the picture (How it works starts there). Trimmed of the unexplored dark below the terrain; otherwise exactly as returned.

Status: alpha — it has only ever run on one install; see Known limits before adopting it. Nothing defaults to anybody's install, the mod-side half is a folder you vendor (responder/), and CI compiles it with nothing of any mod's on the compile line.

Quick start

sh
uv tool install tmodloader-mcp    # or: pip install tmodloader-mcp
# the two paths with no default — every plausible default names somebody's installexport TMODLOADER_SAVE_DIR="/mnt/c/Users/<you>/Documents/My Games/Terraria/tModLoader"export TMODLOADER_MOD_SOURCE="$TMODLOADER_SAVE_DIR/ModSources/<YourMod>"

Then:

  1. Vendor the responder — copy responder/ into your mod's source tree and subclass DevResponder. It is a folder of C#, not a PyPI dependency, because it compiles inside your mod.
  2. Point your MCP client at the server — the block in Using it from Claude Code.
  3. Ask the game something — launch, then diag. Or open with the start_a_session prompt, which lists the worlds and characters that actually exist here.

This needs WSL2 driving a Windows tModLoader — Requirements says why before anything else does.

How it works

The game is asked by writing a file it polls, not by sending it input:

text
┌─────────────────────────────────────────────────────┐│  an agent — Claude Code, or anything speaking MCP   │└──────────────────────┬──────────────────────────────┘                       │  tools · prompts · capture:// resources┌──────────────────────▼──────────────────────────────┐│  tmodloader-mcp — this package (Python, WSL2)       │└──────────────────────┬──────────────────────────────┘                       │  writes  <mod>-capture.trigger                       │  reads   <mod>-diag-<token>.txt,                       │          <mod>-shot-<token>.png, ...┌──────────────────────▼──────────────────────────────┐│  the tModLoader save directory                      ││  plain files on disk — no socket, no keystrokes     │└──────────────────────▲──────────────────────────────┘                       │  polls every few frames, writes                       │  each answer next to its trigger┌──────────────────────┴──────────────────────────────┐│  DevResponder — C# you vendor into YOUR mod,        ││  inside tModLoader, a real game, on Windows         │└─────────────────────────────────────────────────────┘

No synthetic keystrokes, no window focus, and nothing that can be fooled by another window sitting on top of the game. That last point is the reason for the design: OS-level screen capture was tried first and returned a picture of Discord — a window in front of the game — while passing every check available. Reading the game's own back buffer cannot contain another window by construction, not by luck.

Captures name a region and have no default. The frame holds only the game, but that still includes a character name, a world name and any chat on screen, so a request says which corner it wants.

The mod side of that protocol — every filename, what each one contains, and which failures it has to be able to express — is written down in docs/MOD_CONTRACT.md, and implemented in responder/. You can read the contract or vendor the folder; the folder is the same document with a compiler checking it.

Since 0.6.0 replies are tagged: a request may carry a short id the responder echoes back first, so a late answer can never be mistaken for the next request's. Older vendored copies keep working — the tag is only sent to a responder that advertises taking it.

What the mod side answers

These are verbs, driven through the trigger tool, not separate MCP tools.

The reads come with the base class — every consumer serves them, and vendoring an upgrade can never hand your mod a power it did not have before:

VerbWhat it answers
capture / shota photograph of the frame, by named region
diagwhatever your mod chose to report
tilestile-type counts in a rectangle
entitiesNPC, item or projectile counts, filterable by rectangle
findone line per entity: id, position, health
playerswho is connected

The writes are opt-ins, each one line you write in RegisterCommands:

Opt-inVerbs it adds
DevMutationstime weather spawn give teleport settile cleartile despawn
DevCommandBridgecommand commandlist — runs any mod's own registered ModCommands
DevChatchat say

One line each is deliberately the whole mechanism: not a setting, not a marker file, not an environment variable, because each of those can be switched on somewhere other than the source somebody will read when they ask why an NPC appeared in their world. responder/README.md has the detail, including why DevCommandBridge is the answer to "what about an escape hatch" and why there is no reflect_invoke here.

The answers, verbatim

Every line below is real — the same server and client the live checks drive, quoted rather than paraphrased:

text
spawn:1,20        OK: spawned 20 of 20 id=1 at tile 2101,252
time:dusk         sent to a CLIENT:                  REFUSED: "time" changes something the SERVER owns, and a                  client that changed it would be corrected by the next world                  packet - the change would appear to work and then undo                  itself. Send this to the server: time@<server-address>.
give:8,5          sent to the SERVER:                  REFUSED: "give" needs a local player, and a dedicated                  server has none - it runs the world without standing in                  it. Ask a client, by name.
spawn:0,1         REFUSED: 0 is not a NPC id - it is how Terraria spells                  "nothing", so this would have succeeded and done nothing
spawn:banana,5    REFUSED: "banana" is not a positive whole NPC id
time:teatime      REFUSED: "teatime" is not one of dawn, noon, dusk, midnight
tiles:0,0,1000,1000                  REFUSED: 1000 by 1000 is 1000000 tiles, past the limit of                  16384 one query may scan

The refusals are the point. Each one says what was wrong, what would have been right, and — when the verb belongs to the other side of the wire — where to send it instead, because the reader is an agent and an agent retries exactly as well as the refusal explains.

The tool surface

Most of what this does could be a CLI, and where that is true it should stay one — a stateless local binary does not need a protocol in front of it. What earns the surface here is that a running game is not stateless:

A session

ToolWhat it buys over bash
launch / stopSession state across calls
joinA second client into a session that is already running
statusAsking whether a session exists without provoking an error
restartstop → build → launch in the one order that works
inventoryThe worlds, characters and mods launch needs and cannot check

Asking the game

ToolWhat it buys over bash
triggerThe write → poll → timeout → clean-up loop, written once
commandsWhat the mod says it serves, read from the mod, not a copy here
diagStructured fields AND the records under them, not text to sed
wait_untilWaiting for a state on one budget, instead of sleeping a guess
heartbeatWHICH silence — absent, stale, still loading, or not armed

Pictures

ToolWhat it buys over bash
shotA path per call, a whole PNG behind it, refusals as refusals
capturesWhich captures exist, as names — a reader that takes no paths
read_captureThe picture itself, for an agent not on this machine
prune_capturesRemoving captures without a delete loose enough to reach a world

Logs

ToolWhat it buys over bash
logsAny log, filtered — including the run that already rotated away
log_filesWhich logs exist right now, and how many old runs are archived
log_sinceOnly what a log gained, and whether it rotated under you
log_watchBlocking until a line appears, instead of a guessed sleep

Building, the API, and the saves

ToolWhat it buys over bash
build_modEncodes tModLoader's refusal to build while the game is open
api_searchWhat the INSTALLED tModLoader actually exposes, with signatures
save_snapshotCopying the world and characters aside before a run mutates them
save_restorePutting them back, saving what it overwrote so it can be undone
save_snapshotsWhich copies exist, newest first

Those one-liners are where the hand-written version actually went wrong: a pkill pattern that matched its own command line, a readiness check that passed on a killed process's leftover heartbeat, and — for one stretch — a shot row promising a PNG check that did not exist: the file was waited for and renamed, never opened. It exists now, and reads the end that decides — a truncated PNG has a perfectly valid signature, so the trailer is what is checked. The gap was recorded here rather than quietly corrected, because a README is read by people deciding what they no longer have to check.

Two prompts ship with it: diagnose_silence walks the four reasons the mod might not answer, with this install's heartbeat, mod list and logs already read; start_a_session lists the worlds and characters that actually exist here — the two preconditions launch states and cannot check. Both render the failure into the text when the configuration is unusable, because a diagnostic that refuses to render has failed at the one moment it was for.

Captures are also addressable as capture://{name} resources. Both surfaces share one reader that takes a name, never a path, and serves only capture-shaped files inside the save directory — a reader that opened whatever it was handed would be the leak this project exists to prevent.

What it cannot do

There is no headless singleplayer. Terraria has no entry point for it — -join -player -skipselect lands at the main menu, measured rather than assumed. launch("singleplayer") refuses and says so instead of launching something else and letting you believe otherwise. Singleplayer testing needs a human to load a world; the other tools then drive it normally.

That matters more than it sounds: a bug that only appeared in singleplayer shipped once precisely because every harness ran server-plus-client.

There is no bare dedicated server either. An empty server runs no update hooks, so the mod never polls and never answers — measured on one process, changing only whether a client was attached: silent for 90s alone, answering within 30s of a client joining. launch("server") refuses for the same reason singleplayer does — what it promises is a game that can answer, and a server on its own never becomes one. Start one outside this tool if you want a server to join yourself.

Requirements

WSL2 on Windows, driving a Windows tModLoader. This is the one requirement worth reading before the others, because it is not a preference — sessions are listed and killed through Windows' own tasklist.exe and taskkill.exe, and build_mod hands tModLoader a Windows path because it builds inside a Windows process. A native Linux or macOS tModLoader cannot be driven by this as it stands. The configuration check every tool runs first says so by name rather than failing later on a missing file in System32; if you are on WSL and those tools live somewhere unusual, set TMODLOADER_TASKLIST, TMODLOADER_TASKKILL and TMODLOADER_POWERSHELL.

  • Python 3.12+
  • tModLoader installed (1.4.4.9 is what this is tested against)
  • A mod embedding the trigger-file responder — copy responder/ into your mod's source tree and subclass DevResponder
  • A .NET SDK, for api_search only — the index is built by a small C# tool. Everything else works without one.

Install

sh
uv tool install tmodloader-mcp    # or: pip install tmodloader-mcp                                  # or none at all: uvx tmodloader-mcp

The package is the Python half only. The responder is not on PyPI and could not usefully be — it is C# that compiles inside your mod — so it is vendored from responder/ in the repository, however you obtained the package.

Configuration

Every path is an environment variable. Two are required, because every plausible default for them names somebody's own install:

VariableMeaning
TMODLOADER_SAVE_DIRRequired. Where the mod writes artifacts
TMODLOADER_MOD_SOURCERequired. Mod source directory (WSL path)
TMODLOADER_DIRtModLoader install; defaults to Steam's layout
TMODLOADER_WORLD_WINDefault world, as Windows spells it — see below
TMODLOADER_MOD_SOURCE_WINUsually leave unset — see below
TMODLOADER_MOD_NAMEUsually leave unset — see below
sh
export TMODLOADER_SAVE_DIR="/mnt/c/Users/<you>/Documents/My Games/Terraria/tModLoader"export TMODLOADER_MOD_SOURCE="$TMODLOADER_SAVE_DIR/ModSources/<YourMod>"

The required two have no default on purpose: a default pointing at the author's disk does not fail on yours — it resolves, worst case to something that exists, and the server drives an install you never chose. Both unset variables are reported together, so this costs one restart, not two.

TMODLOADER_WORLD_WIN is the world launch loads when you do not pass one. It has no default either; with neither set, launch refuses and lists the worlds actually in your save directory, with the Windows paths it wants. inventory answers the same question without launching anything.

TMODLOADER_MOD_NAME — when the derived name is wrong, and what the name keeps apart

TMODLOADER_MOD_NAME is the mod's internal name, which every artifact filename is built from: <modname>-diag-<token>.txt, <modname>-shot-<token>.png, lowercased, where <token> identifies which player's client wrote it — except <modname>-capture.trigger and <modname>-commands.txt, which stay one name shared by every client (see docs/MOD_CONTRACT.md for why). tModLoader takes the mod's name from the source folder, so it is derived from TMODLOADER_MOD_SOURCE and only needs setting for a checkout whose folder is named something other than the mod. Deriving it is also what keeps two mods driven from one machine out of each other's trigger files — they share a save directory.

TMODLOADER_MOD_SOURCE_WIN — only for a mod source living outside /mnt/<drive>

TMODLOADER_MOD_SOURCE_WIN is the mod source as Windows sees it, which -build needs because tModLoader compiles inside a Windows process with no /mnt/c. It is derived from TMODLOADER_MOD_SOURCE, so setting that one is enough for a source on a drive mount. Set it yourself only if your mod source lives outside /mnt/<drive>, where there is no drive letter to translate to and the server will ask for it by name.

The two describe one directory. If you set both to different places the server refuses to start and says so, rather than driving one and building the other.

Using it from Claude Code

A .mcp.json ships with the repository, so a session started in this directory finds the server:

json
{  "mcpServers": {    "tmodloader": {      "command": "uv",      "args": ["run", "tmodloader-mcp"],      "env": {        "TMODLOADER_SAVE_DIR": "${TMODLOADER_SAVE_DIR}",        "TMODLOADER_MOD_SOURCE": "${TMODLOADER_MOD_SOURCE}"      }    }  }}

The two paths are read from your environment rather than written down — a committed config with real paths would be one person's paths in everybody's checkout. Export them where the client is launched, not only in an interactive shell: the substitution is the client's, against its own environment, and a value passed through as literal ${TMODLOADER_SAVE_DIR} is treated as absent and reported by that name. claude mcp list names any that are missing.

If the server reports a variable missing that you know you exported — the usual cause is a client older than the export

A process's environment is a copy taken when it starts, and nothing outside can add to it afterwards. So a long-lived parent — a daemon, an agent host, a desktop session — hands every client it spawns the environment it had on the day it started, however long ago that was, and adding the variables to your profile today does not reach it. The symptom is a shell where env | grep TMODLOADER prints all three sitting next to a server that sees none of them. Restarting the client is not enough if the thing that spawned the client is the stale one; restart that.

Nothing can be repaired from inside a running session: the value was gone before the process started.

A project-scoped .mcp.json needs approving once — Claude Code will not run a server a repository asked it to run without being told to. Start claude in this directory and accept the prompt.

To drive the harness from the directory where you actually develop your mod, copy the block into that project's .mcp.json and point --directory at this checkout:

json
"args": ["run", "--directory", "/path/to/tmodloader-mcp", "tmodloader-mcp"]

Or, with the package from PyPI, no checkout at all:

json
"command": "uvx","args": ["tmodloader-mcp"]

The repository's own .mcp.json stays on uv run deliberately — inside this checkout you want the code in front of you, not the release behind it.

Known limits

Everything below is a fact about this repository rather than a plan; the struck-through history this section replaced lives in CHANGELOG.md, which is where a changelog belongs.

The thing most worth knowing about this project is that its hardest bugs were found by RUNNING it. Two clients overwriting each other's answers, a capture lock bounded by a guess, a dedicated server with no address to be told apart by, and — most recently — a save-snapshot feature whose entire premise turned out to be false when somebody finally measured it. The unit suite passed against every one of those. The changelog is the record.

It has only ever run on one install. One machine, one tModLoader (1.4.4.9), one world, one character. Every live check in tests/ drives a real game rather than a mock, which is the strongest evidence this project has — and it is still evidence from a single configuration. That is what the alpha classifier is for, and the first thing an outside user is likely to find is something install-specific.

Two dedicated servers racing for one trigger: observed once, not proven. Two servers on one save directory — different ports, different worlds, both polling the same trigger file — served six alternating addressed requests, each answered by the server it named, never by the other, always consumed (tests/live_race_check.py). One machine, one run: evidence the claim protocol holds under a real race, not a proof it always will.

There is no escape hatch, deliberately. Other harnesses ship reflect_invoke or execute_code. command is the answer here: it runs a mod's OWN registered ModCommands, which the mod already decided existed, named, and gave a usage line. That keeps every reachable action published, typed and refusable. Arbitrary evaluation would buy unlimited reach and throw that away, so a question nobody wrote a verb for still costs an edit, a rebuild and a relaunch.

A stopped session saves nothing. stop force-kills, so a run that changes the world usually leaves no trace on disk — measured, not assumed. Do not rely on that: a run long enough to autosave, or a graceful exit, does write. Take a save_snapshot before anything that mutates a world you care about.


[圖片]

MIT · what running it taught, in order · the protocol, written down · a template mod to start from

來源:README.md,提交 8e4473d

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

1
  1. v0.6.3最新Sep 17, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分