tmodloader-mcp について
tmodloader-mcp は、Mod に組み込む responder を介して実行中の tModLoader を操作・確認する Python 製 MCP サーバーです。セッション管理や構造化された状態照会、ゲームのバックバッファからの撮影に対応し、WSL2 から Windows 版 tModLoader を動かす必要があります。
主な機能
- 01ゲームセッションの起動と管理
- 02エンティティ、タイル、プレイヤー、診断情報の照会
- 03ゲームのバックバッファから画像を取得
- 04ログの確認とゲーム状態の待機
- 05Mod のビルドとインストール済み API の確認
- 06セーブのスナップショットと復元
活用シーン
- 01実行中の Mod 環境を調査
- 02前面ウィンドウの影響を受けずにゲーム画面を取得
- 03Mod の動作を試しセーブのスナップショットを復元
- 04ゲームと Mod 間の応答停止を診断
MCP 対応
エージェントが実行中の tModLoader を操作するための MCP ツール、プロンプト、capture:// リソースを提供します。ゲームとの通信には、Mod に組み込む trigger ファイル方式の responder を使います。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
24掲載時のツール一覧 · 2 週間前に更新
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
| timeout | number | いいえ |
詳細
tmodloader-mcp
エージェントから起動中の tModLoader インスタンスを操作できます — 起動し、質問し、スクリーンショットを撮影し、その状態を構造化データとして読み取ります。
ゲームエンジンにも MCP サーバーが登場しています — Unity、Unreal、Godot、Defold にはすべて MCP サーバーがあり、アシスタントはプロンプトから推測するのではなく、実際のシーンを見られます。tModLoader にはこれまでありませんでした。これはそのためのものです。
このツールはスクリーンショットを自分で準備しました —
time:noon、teleport:spawn、spawn:1,25、
shot:full — ゲームのバックバッファーから読み取るため、ゲームの前にウィンドウがあっても画像に写りません
(まずは仕組みをご覧ください)。地形の下にある未探索の暗い部分は切り取っています。それ以外は返された内容そのままです。
ステータス:アルファ版 — これまで動作したのは1つのインストール環境だけです。採用する前に 既知の制限をご確認ください。どのインストール環境もデフォルトでは指定されません。Mod 側の部分はベンダーとして取り込むフォルダーであり (
responder/)、CI はコンパイル時にいずれの Mod のものも使用せずにコンパイルします。
クイックスタート
uv tool install tmodloader-mcp # or: pip install tmodloader-mcp
# the two paths with no default — every plausible default names somebody's install
export TMODLOADER_SAVE_DIR="/mnt/c/Users/<you>/Documents/My Games/Terraria/tModLoader"
export TMODLOADER_MOD_SOURCE="$TMODLOADER_SAVE_DIR/ModSources/<YourMod>"
次に、以下を行います。
- レスポンダーをベンダーとして取り込む —
responder/を Mod のソースツリーにコピーし、DevResponderをサブクラス化します。これは C# のフォルダーであり、PyPI の依存関係ではありません。あなたの Mod 内でコンパイルされるためです。 - MCP クライアントをサーバーに接続する — Claude Code から使うのブロックをご覧ください。
- ゲームに質問する —
launch、続いてdiagを実行します。または、start_a_sessionプロンプトを使って開くと、ここに実際に存在するワールドとキャラクターが一覧表示されます。
これにはWindows 版 tModLoader を操作する WSL2が必要です — その理由は、ほかの説明に先立って要件で説明しています。
仕組み
ゲームへの要求は、入力を送るのではなく、ゲームがポーリングするファイルを書き込むことで行います。
┌─────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────┘
合成キー入力も、ウィンドウのフォーカスも、ゲームの上に別のウィンドウが重なっていてもだまされるような仕組みもありません。これがこの設計の理由です。最初に OS レベルの画面キャプチャを試したところ、あらゆるチェックをすり抜けたうえで、ゲームの前面にある Discord の画像が返されました。ゲーム自身のバックバッファを読み取れば、運に頼らず、仕組み上ほかのウィンドウが入り込むことはありません。
キャプチャでは領域を指定し、デフォルトはありません。フレームに含まれるのはゲームだけですが、それでもキャラクター名、ワールド名、画面上のチャットが含まれるため、リクエストで必要な隅を指定します。
このプロトコルの Mod 側については、各ファイル名、その内容、表現できる必要があるエラーを
docs/MOD_CONTRACT.md
に記載し、
responder/
に実装しています。契約を読むことも、フォルダーをベンダリングすることもできます。このフォルダーは、コンパイラーによるチェックが加わった同じドキュメントです。
0.6.0 以降、応答にはタグが付きます。リクエストに短い ID を含めると、responder が最初にその ID をエコーするため、遅れて届いた応答を次のリクエストへの応答と取り違えることはありません。古いベンダリング済みコピーも引き続き動作します。タグを送るのは、受け取ると通知した responder に限られます。
Mod 側が応答する内容
以下は trigger ツールを通して実行する動詞であり、個別の MCP ツールではありません。
読み取り機能は基底クラスに含まれています。すべてのコンシューマーがこれらを提供し、アップグレードをベンダリングしても、Mod に以前なかった権限が追加されることはありません。
| 動詞 | 応答する内容 |
|---|---|
capture / shot | 名前で指定した領域のフレーム画像 |
diag | Mod が報告するよう選んだ内容 |
tiles | 矩形領域内のタイルタイプ別の数 |
entities | NPC、アイテム、または投射物の数。矩形領域でフィルター可能 |
find | エンティティごとに 1 行:ID、位置、体力 |
players | 接続中のプレイヤー |
書き込み機能はオプトイン方式で、それぞれ RegisterCommands に 1 行追加して有効にします。
| オプトイン | 追加される動詞 |
|---|---|
DevMutations | time weather spawn give teleport settile cleartile despawn |
DevCommandBridge | command commandlist — 任意の Mod 自身が登録した ModCommand を実行します |
DevChat | chat say |
意図的に、仕組みは各 1 行だけです。設定でも、マーカーファイルでも、環境変数でもありません。そうした仕組みはいずれも、NPC がワールドに現れた理由を調べる人が読むソース以外の場所で有効化できてしまうからです。
responder/README.md
には、「非常口はどうするのか」という問いへの答えが DevCommandBridge である理由や、ここに reflect_invoke がない理由など、詳しい説明があります。
応答の実例
以下の各行はすべて実際のものです。ライブチェックで使われるものと同じサーバーおよびクライアントからの応答を、言い換えずに引用しています。
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
拒否応答こそが重要です。それぞれ、何が間違っていたのか、どうすればよかったのか、そしてその動詞が通信の相手側に属する場合は代わりにどこへ送ればよいのかを示します。読み手はエージェントであり、エージェントは拒否応答の説明どおりに再試行します。
ツールの構成
これが行っていることの大部分は、CLI にすることもできます。CLI で済むものは、CLI のままにすべきです。ステートレスなローカルバイナリの前に、プロトコルを置く必要はありません。ここに独自のインターフェースを設ける価値があるのは、実行中のゲームがステートレスではないからです。
セッション
| ツール | bash を使う場合より便利な点 |
|---|---|
launch / stop | 呼び出しをまたいでセッション状態を保持します |
join | すでに実行中のセッションに、2 つ目のクライアントを参加させます |
status | エラーを発生させずに、セッションが存在するか確認します |
restart | 正しく動作する唯一の順序で、stop → build → launch を実行します |
inventory | launch に必要で、launch からは確認できないワールド、キャラクター、MODを表示します |
ゲームへの問い合わせ
| ツール | bash を使う場合より便利な点 |
|---|---|
trigger | 書き込み → ポーリング → タイムアウト → クリーンアップのループを、一度だけ実装しています |
commands | MOD が提供すると宣言している内容を、この場でコピーしたものではなく、MOD から読み取ります |
diag | 構造化されたフィールドと、その下にあるレコードの両方を取得します。sed で処理するテキストではありません |
wait_until | 勘で待機時間を決めてスリープするのではなく、1 つの予算内で状態を待ちます |
heartbeat | どのような無応答なのかを判別します。未検出、古い状態、読み込み中、または未起動のいずれかです |
画像
| ツール | bash を使う場合より便利な点 |
|---|---|
shot | 呼び出しごとのパス、その背後にある PNG 全体、拒否は拒否として返します |
captures | キャプチャの名前を一覧表示します。パスを必要としない読み取り専用ツールです |
read_capture | このマシンを使っていないエージェント向けに、画像そのものを取得します |
prune_captures | ワールドにまで手が届くような緩い削除機能を使わずに、キャプチャを削除します |
ログ
| ツール | bash を使う場合より便利な点 |
|---|---|
logs | すでにローテーションで消えた実行分も含め、あらゆるログをフィルタリングできます |
log_files | 現在存在するログと、アーカイブ済みの過去の実行数を確認できます |
log_since | ログに追加された内容だけを取得し、その間にローテーションが起きたかも確認できます |
log_watch | 勘でスリープするのではなく、行が現れるまで待機します |
ビルド、API、セーブデータ
| ツール | bash を使う場合より便利な点 |
|---|---|
build_mod | ゲーム実行中はビルドできないという tModLoader の制約を処理します |
api_search | インストール済みの tModLoader が実際に公開している内容を、シグネチャ付きで確認できます |
save_snapshot | 実行によって変更が加わる前に、ワールドとキャラクターを別の場所へコピーします |
save_restore | 上書きされた内容を元に戻せるよう保存したうえで、ワールドとキャラクターを復元します |
save_snapshots | 存在するコピーを新しい順に一覧表示します |
こうした一行コマンドこそ、手書きの実装が実際に失敗した箇所です。自分自身のコマンドラインにマッチする pkill のパターン、kill 済みプロセスの残したハートビートを見て成功してしまう起動確認、そして一時期のことですが、存在しない PNG チェックを約束していた shot の行。ファイルの生成を待って名前を変更するだけで、開いてはいませんでした。今は PNG を開き、判定を左右する末尾を読み取ります。切り詰められた PNG でもシグネチャは完全に有効なので、確認するのはトレーラーです。この不足は、黙って修正するのではなく、ここに記録しました。README は、何をもう確認しなくてよいのかを判断する人が読むものだからです。
2 つのプロンプトが付属します。diagnose_silence は、このインストール環境のハートビート、Mod リスト、ログをすでに読み込んだうえで、Mod が応答しない可能性のある 4 つの理由を確認します。start_a_session は、ここに実在するワールドとキャラクターを一覧表示します。どちらも launch が前提条件として示すものの、確認できない項目です。設定を利用できない場合は、どちらも失敗の内容をテキストに表示します。診断結果を表示できない診断では、本来必要とされる唯一の場面で役に立たないからです。
キャプチャには、capture://{name} リソースとしてもアクセスできます。どちらの方法でも、パスではなく名前を受け取り、セーブディレクトリ内にあるキャプチャ形式のファイルだけを提供する共通のリーダーを使います。渡されたものを何でも開くリーダーは、このプロジェクトが防ごうとしている情報漏えいそのものです。
できないこと
ヘッドレスのシングルプレイヤーはありません。 Terraria にはそのためのエントリーポイントがありません。-join -player -skipselect を指定するとメインメニューに戻ることは、推測ではなく実測で確認しています。launch("singleplayer") は拒否し、その理由を伝えます。別のものを起動して、そうとは知らずにシングルプレイヤーが起動したと思わせることはありません。シングルプレイヤーのテストでは、人間がワールドを読み込む必要があります。その後は、ほかのツールで通常どおり操作できます。
これは見た目以上に重要です。シングルプレイヤーでしか発生しないバグが、すべてのテストハーネスでサーバーとクライアントを使っていたために、実際に一度リリースされました。
通常の専用サーバーもありません。 空のサーバーでは更新フックが実行されないため、Mod がポーリングせず、応答もしません。クライアントを接続するかどうかだけを変えて 1 つのプロセスで測定したところ、サーバー単独では 90 秒間無応答で、クライアントが参加してから 30 秒以内に応答しました。launch("server") が拒否される理由はシングルプレイヤーの場合と同じです。これは応答可能なゲームを起動することを約束していますが、サーバー単独ではその状態になりません。自分で参加するサーバーが必要な場合は、このツールの外部で起動してください。
要件
Windows 上の WSL2 から、Windows 版 tModLoader を操作します。 これは、ほかの要件より先に確認する価値のある唯一の要件です。単なる推奨ではありません。セッションの一覧表示と終了には Windows 標準の tasklist.exe と taskkill.exe を使い、build_mod は Windows プロセス内でビルドするため、tModLoader に Windows のパスを渡します。ネイティブの Linux 版や macOS 版の tModLoader は、現状ではこのツールから操作できません。各ツールが最初に実行する設定チェックでその旨を明示するため、後になって System32 にファイルが見つからず失敗することはありません。WSL を使っていて、これらのツールが通常とは異なる場所にある場合は、TMODLOADER_TASKLIST、TMODLOADER_TASKKILL、TMODLOADER_POWERSHELL を設定してください。
- Python 3.12 以降
- tModLoader のインストール(テスト対象は 1.4.4.9 です)
- トリガーファイル応答機能を組み込んだ Mod。自分の Mod のソースツリーに
responder/をコピーし、DevResponderをサブクラス化してください。 api_searchのみ、.NET SDK が必要です。インデックスは小さな C# ツールで作成します。それ以外の機能は SDK なしで動作します。
インストール
uv tool install tmodloader-mcp # or: pip install tmodloader-mcp
# or none at all: uvx tmodloader-mcp
このパッケージに含まれるのは Python 側のみです。レスポンダーは PyPI で公開されておらず、公開する実用的な方法もありません。あなたの Mod 内でコンパイルする C# コードだからです。そのため、パッケージの入手方法にかかわらず、リポジトリにある responder/ を同梱してください。
設定
すべてのパスは環境変数です。もっともらしい既定値を指定すると、誰かの環境にあるインストール先を指してしまうため、2 つは必須です。
| 変数 | 意味 |
|---|---|
TMODLOADER_SAVE_DIR | 必須。 Mod が成果物を書き込む場所 |
TMODLOADER_MOD_SOURCE | 必須。 Mod のソースディレクトリ(WSL パス) |
TMODLOADER_DIR | tModLoader のインストール先。既定値は Steam の構成 |
TMODLOADER_WORLD_WIN | 既定のワールド。Windows での表記です。下記参照 |
TMODLOADER_MOD_SOURCE_WIN | 通常は未設定のままにします。下記参照 |
TMODLOADER_MOD_NAME | 通常は未設定のままにします。下記参照 |
export TMODLOADER_SAVE_DIR="/mnt/c/Users/<you>/Documents/My Games/Terraria/tModLoader"
export TMODLOADER_MOD_SOURCE="$TMODLOADER_SAVE_DIR/ModSources/<YourMod>"
必須の 2 つに既定値がないのは意図的です。作成者のディスクを指す既定値は、あなたの環境ではエラーになりません。最悪の場合は実在する場所に解決され、サーバーがあなたの選んでいないインストール先を操作してしまいます。未設定の変数は両方まとめて報告されるため、再起動は 2 回ではなく 1 回で済みます。
TMODLOADER_WORLD_WIN は、launch にワールドを渡さなかったときに読み込まれるワールドです。これにも既定値はありません。どちらも設定されていない場合、launch は起動を拒否し、セーブディレクトリに実際にあるワールドを、必要な Windows パスとともに一覧表示します。inventory を使えば、何も起動せずに同じ情報を確認できます。
TMODLOADER_MOD_NAME — 派生名が正しくない場合と、
名前によって分離されるもの
TMODLOADER_MOD_NAME は Mod の内部名で、すべての成果物ファイル名の基になります。ファイル名は小文字で、<modname>-diag-<token>.txt、<modname>-shot-<token>.png となります。<token> は、どのプレイヤーのクライアントが書き込んだかを識別します。ただし、<modname>-capture.trigger と <modname>-commands.txt は例外で、すべてのクライアントで共有される単一の名前のままです(理由は
docs/MOD_CONTRACT.md
を参照してください)。
tModLoader はソースフォルダーから Mod の名前を取得するため、TMODLOADER_MOD_SOURCE から自動的に導出されます。チェックアウトしたフォルダー名が Mod 名と異なる場合に限り、設定が必要です。名前を導出することで、1 台のマシンから操作する 2 つの Mod が互いのトリガーファイルを使わないようにもなります。両者はセーブディレクトリを共有するためです。
TMODLOADER_MOD_SOURCE_WIN — Mod のソースが
/mnt/<drive> の外にある場合に限ります
TMODLOADER_MOD_SOURCE_WIN は Windows から見た Mod のソースで、tModLoader は /mnt/c のない Windows プロセス内でコンパイルするため、-build に必要です。これは TMODLOADER_MOD_SOURCE から自動的に導出されるため、ドライブのマウント先にあるソースなら、そちらを設定するだけで十分です。/mnt/<drive> の外に Mod のソースがある場合に限り、自分で設定してください。その場合は変換先となるドライブ文字がないため、サーバーから名前を指定して設定を求められます。
この 2 つは、同じディレクトリを指します。異なる場所を指定した場合、サーバーは一方を操作してもう一方でビルドするのではなく、その旨を示して起動を拒否します。
Claude Code から使用する
リポジトリには .mcp.json が同梱されているため、このディレクトリで開始したセッションはサーバーを検出します。
{
"mcpServers": {
"tmodloader": {
"command": "uv",
"args": ["run", "tmodloader-mcp"],
"env": {
"TMODLOADER_SAVE_DIR": "${TMODLOADER_SAVE_DIR}",
"TMODLOADER_MOD_SOURCE": "${TMODLOADER_MOD_SOURCE}"
}
}
}
}
2 つのパスは書き留めるのではなく、環境から読み取ります。実際のパスを記述した設定をコミットすると、誰か 1 人のパスが全員のチェックアウトに含まれてしまいます。環境変数は、対話型シェルだけでなく、クライアントの起動時にエクスポートしてください。置換を行うのはクライアントであり、クライアント自身の環境を参照します。${TMODLOADER_SAVE_DIR} がリテラルのまま渡されると、未設定として扱われ、その名前が報告されます。claude mcp list で未設定の環境変数を確認できます。
エクスポートした環境変数があるのに、サーバーが未設定と報告する場合 — よくある原因は、エクスポートより前に起動したクライアントです
プロセスの環境は起動時にコピーされ、その後、外部から追加することはできません。そのため、デーモン、エージェントホスト、デスクトップセッションなど、長時間稼働している親プロセスは、どれほど時間が経っていても、起動した日に持っていた環境をすべての子クライアントに引き渡します。今日プロファイルに環境変数を追加しても、その環境は届きません。症状として、env | grep TMODLOADER では 3 つすべてが表示されるシェルの一方で、サーバーには 1 つも見えていないことがあります。クライアントを起動したプロセスが古い環境を持っている場合、クライアントを再起動するだけでは不十分です。そのプロセスを再起動してください。
実行中のセッション内で問題を修復することはできません。プロセスの起動前に、環境変数の値が失われているためです。
プロジェクトスコープの .mcp.json は、最初に一度だけ承認が必要です。Claude Code は、リポジトリからの指示だけではサーバーを実行しません。このディレクトリで claude を起動し、プロンプトを承認してください。
実際に MOD を開発しているディレクトリからハーネスを実行するには、ブロックをそのプロジェクトの .mcp.json にコピーし、--directory にこのチェックアウトのパスを指定してください。
"args": ["run", "--directory", "/path/to/tmodloader-mcp", "tmodloader-mcp"]
または、PyPI のパッケージを使えば、チェックアウトは不要です。
"command": "uvx",
"args": ["tmodloader-mcp"]
このリポジトリの .mcp.json が意図的に uv run を使っているのは、リポジトリ内ではリリース版ではなく、手元のコードを使うためです。
既知の制限
以下はすべて、このリポジトリに関する事実であり、計画ではありません。このセクションで置き換えられた取り消し線付きの履歴は、
変更履歴を記載する場所である
CHANGELOG.md
にあります。
このプロジェクトについて最も知っておくべきことは、最も厄介なバグは実際に実行して見つかったということです。 2 つのクライアントが互いの回答を上書きする問題、推測に基づく上限が設けられたキャプチャロック、識別に使えるアドレスがない専用サーバー、そして最近では、誰かがようやく測定した結果、前提そのものが誤りだと判明したセーブスナップショット機能。これらすべてに対して、ユニットテストスイートは合格していました。その記録が変更履歴です。
これまで実行されたのは、たった 1 つのインストール環境だけです。 1 台のマシン、1 つの tModLoader
(1.4.4.9)、1 つのワールド、1 人のキャラクター。tests/ 内のライブチェックはすべて、モックではなく実際のゲームを動かします。これはこのプロジェクトが持つ最も強力な証拠ですが、あくまで単一の構成から得られた証拠です。アルファ版の分類はそのためにあり、外部ユーザーが最初に見つけるのは、インストール環境固有の問題である可能性が高いです。
1 つのトリガーをめぐって 2 台の専用サーバーが競合:1 度観測されたが、証明はされていません。 1 つのセーブディレクトリを共有する 2 台のサーバーが、異なるポートと異なるワールドを使用し、同じトリガーファイルをポーリングしながら、宛先を指定した 6 件のリクエストを交互に処理しました。それぞれのリクエストには指定されたサーバーが回答し、もう一方のサーバーが回答することはなく、すべてのリクエストが消費されました(tests/live_race_check.py)。1 台のマシンで 1 回実行した結果は、実際の競合状態でも要求の振り分けプロトコルが機能することを示す証拠であり、常に機能することの証明ではありません。
意図的に、回避手段はありません。 他のハーネスにはreflect_invokeやexecute_codeが用意されています。ここでの答えはcommandです。これは、MOD が登録した独自のModCommandのうち、MOD がすでに存在を認め、名前を付け、使用方法を与えたものを実行します。これにより、到達可能なすべてのアクションが公開され、型付けされ、拒否可能な状態に保たれます。任意のコード評価を導入すれば、無制限の操作範囲を得る代わりに、こうした保証は失われます。そのため、動詞が用意されていない操作を実行するには、依然として編集、再ビルド、再起動が必要です。
セッションを停止しても、何も保存されません。 stopは強制終了するため、ワールドを変更する実行でも、通常はディスク上に痕跡が残りません。これは推測ではなく、測定された結果です。ただし、これを当てにしないでください。自動保存が行われるほど長く実行した場合や、正常に終了した場合は、書き込みが行われます。大切なワールドを変更する前に、save_snapshotを実行してください。
MIT · 実行から得た学びを時系列で記録 · 文書化されたプロトコル · 開発の出発点となるテンプレート MOD
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。