# tmodloader-mcp (pypi · tmodloader-mcp)

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

- Trust score: 54/100 (low)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-09-20

## Components

- pypi · `tmodloader-mcp`: 54/100 (this document), [markdown](https://verifymcp.io/servers/musharna-tmodloader-mcp/tmodloader-mcp.md), [page](https://verifymcp.io/servers/musharna-tmodloader-mcp/tmodloader-mcp)

## Channel facts

- Registry: `pypi`
- Package: `tmodloader-mcp`
- Version: `0.6.3`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-09-20.

- **Supply Chain Security**: 50/100
  - Malware scan not yet available for this package.
  - No known CVEs affecting this package version or its production dependencies.
  - Runs hatchling.build at install time, a recognised native-build step with no shell scripting around it.
  - 1 of 29 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 45/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 3 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 77/100
  - 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 6400 tokens (~266/item across 24 items; 24 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 0/100
  - Stability not yet verified: not enough scan history yet (needs a 30-day window).
- **Tool Coverage**: 71/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 0% of tool parameters carry a description.
  - Structured output schemas are declared (96% of tools); any adoption earns full credit.
- **Tool Safety**: 100/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - All 3 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation.
  - An AI judge read all 25 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a current MCP spec version (2026-07-28).

**Unverified: 1 category.** A category scored 0 because we could not verify it: a data source with nothing on this package, evidence we could not reach, or a check we could not run. We only credit what we can confirm.

## Install

### How do I install the tmodloader-mcp server?

tmodloader-mcp runs locally as a PyPI package, launched with uvx tmodloader-mcp. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add musharna-tmodloader-mcp -- uvx tmodloader-mcp
```

### Cursor

```json
{
  "mcpServers": {
    "musharna-tmodloader-mcp": {
      "command": "uvx",
      "args": [
        "tmodloader-mcp"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "musharna-tmodloader-mcp": {
      "command": "uvx",
      "args": [
        "tmodloader-mcp"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add musharna-tmodloader-mcp -- uvx tmodloader-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "musharna-tmodloader-mcp": {
      "type": "local",
      "command": [
        "uvx",
        "tmodloader-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add musharna-tmodloader-mcp --command uvx --arg tmodloader-mcp
```

### Hermes

```yaml
mcp_servers:
  musharna-tmodloader-mcp:
    command: "uvx"
    args: ["tmodloader-mcp"]
```

### Netclaw

```json
{
  "McpServers": {
    "musharna-tmodloader-mcp": {
      "Transport": "stdio",
      "Command": "uvx",
      "Arguments": [
        "tmodloader-mcp"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add musharna-tmodloader-mcp -t stdio -c uvx -a tmodloader-mcp
```

### Other

```json
{
  "mcpServers": {
    "musharna-tmodloader-mcp": {
      "command": "uvx",
      "args": [
        "tmodloader-mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-09-17 (score 54)

First indexed and scored.

## MCP tools (24)

### `build_mod` (~164 tokens)

Build the mod

Compile the configured mod source into a .tmod.

Args:
    timeout: Seconds to wait for the compile. A large mod on a slow machine
        can outlast the default, and a build that runs out of time says so
        rather than reporting a compile failure with no errors in it.

tModLoader REFUSES to build while the game is open, and says so with an
error that otherwise reads like a compile failure. That case is reported as
itself, with the fix, rather than as a broken build — which is the
difference between closing the game and hunting a syntax error that is not
there.

Success is read from the output, not the exit code, which is not reliable
here.

Input parameters:

- `timeout` (number)

Output parameters:

- `errors` (integer)
- `game_was_open` (boolean)
- `ok` (boolean)
- `summary` (string)
- `warnings` (integer)

### `launch` (~334 tokens)

Launch a game session

Start tModLoader and wait until it can actually answer.

Args:
    mode: "server_client" — a server plus one joined client. It is the only
        mode there is, and the only way to observe what a CLIENT sees, which
        is where most sync bugs live. The other two are refused because the
        engine cannot satisfy them: "singleplayer" has no headless entry
        point, and "server" alone never ticks, so the mod never polls and
        never answers.
    port: Server port. Change it only if something else holds the default.
    player: Character name for the client. Must already exist — `-player`
        does not create one, and a duplicate name is kicked.
    world: WINDOWS path to a `.wld`, overriding TMODLOADER_WORLD_WIN. A WSL
        path is refused rather than tried: tModLoader runs as a Windows
        process, cannot resolve /mnt/c, and the only symptom is a readiness
        timeout blaming the heartbeat.
    timeout: Seconds to wait for readiness. Raise it on a slow machine or a
        large world — the default assumes neither.

Waits for a heartbeat that is BOTH recent and reporting a live world. Those
fail differently — a stale-but-ready heartbeat means the process died, a
fresh-but-not-ready one means it is still loading — and checking only
existence conflates them.

Input parameters:

- `mode` (string)
- `player` (string)
- `port` (integer)
- `timeout` (number)
- `world`

Output parameters:

- `mode` (string)
- `player` (string)
- `port` (integer)
- `started_pids` (array)
- `world`

### `join` (~284 tokens)

Join a second client to the running session

Bring another character into the session that is already running.

Args:
    player: Character name. Must already exist — `-player` does not create
        one — and must not be one this session already has, in any casing.
    timeout: Seconds to wait for that client to report a live, world-ready
        heartbeat of its own.

The protocol has supported several clients since answers became per-player;
the LIFECYCLE supported one, so the arrangement that work exists to make
safe could only be reached by spawning a game by hand. This is that, with
the waiting done properly.

It waits for THIS client, not for a process. A new pid says something
started — not that a character loaded, that the join was accepted, or that
a world is under it. And it watches only that player's own tokened
heartbeat: the unsuffixed `<mod>-hooks.txt` is a shared slot holding
whichever client booted last, so accepting it would return against the
heartbeat of the game that was already here.

Address the new client by name — `diag(target=...)`, `shot(target=...)` —
which already works, because addressing was never the half that was
missing. `stop` takes it down with everything else the session started.

Input parameters:

- `player` (string, required)
- `timeout` (number)

Output parameters:

- `joined` (array)
- `player` (string)
- `started_pids` (array)

### `trigger` (~520 tokens)

Send a dev trigger to the game

Ask the running game to do something, and return what it said.

Args:
    command: One of the dev commands THIS mod serves — call `commands` to
        see them, since they are the running mod's rather than a list kept
        here. A word it does not serve is refused before anything is
        written to disk: a game that does not recognise one does nothing,
        and from outside that is indistinguishable from a hang.
    target: Address the request to one player by name. Every request is
        already addressed to this session's own player by default, so pass
        this only to ask a DIFFERENT client — one another session drives on
        the same machine.
    argument: Only some commands read one — `commands` says which. Passing
        one to a command that takes none is refused here, because the mod
        would refuse it too and that costs a round trip.
    server: Send to the dedicated server rather than the client. Some
        commands are server-authoritative and refuse on a client, and each
        side publishes its own list.
    timeout: Seconds to wait for the game's reply. A command that does real
        work on a large world can outlast the default, and for `capture`
        that is now safe at any value: the capture lock records the
        deadline this argument implies, so another session waits it out
        instead of guessing from the lock's age. Raising it no longer
        trades a slow capture against a collision.

\`refused` is reported separately from `ok`: a refusal is the mod
deliberately saying no, and treating it as success is how a rejected action
reads as a completed one.

\`note` is usually null and is the one field with no other way to reach you:
something that happened on the way to this reply which the reply itself
cannot show. Currently it says a capture lock was broken to take this
picture — captures are serialised across sessions sharing a save directory,
and this one's holder was judged gone. It names WHICH rule judg…

Input parameters:

- `argument`
- `command` (string, required)
- `server` (boolean)
- `target`
- `timeout` (number)

Output parameters:

- `command` (string)
- `note`
- `ok` (boolean)
- `refused` (boolean)
- `text` (string)

### `commands` (~246 tokens)

What the running mod serves

What this side's mod says it serves, read from the mod itself.

The list is published by the responder when it loads, not assembled here.
This harness used to carry its own copy of one mod's twelve commands and its
own belief about which read an argument — facts that belonged to running C#
and drifted the moment either side changed alone.

\`responder` IS THE USEFUL FIELD when something is wrong. False means no list
was published: the mod is not loaded, or is a build with the dev bridge
compiled out. That is a different answer from a game still starting, and it
used to arrive as a readiness timeout, which names the wrong thing entirely —
it reads as slow rather than as never going to answer.

A list that exists but cannot be read is an ERROR rather than `responder:
false`, because it means a responder IS running and this side cannot
understand it — a version mismatch, which needs a human rather than a wait.

Args:
    server: Ask the dedicated server rather than the client. Each side
        publishes its own list, and they are not always the same.

Input parameters:

- `server` (boolean)

Output parameters:

- `commands` (array)
- `note`
- `responder` (boolean)

### `diag` (~234 tokens)

Read the game's state

Ask one side of the session what it currently sees, parsed.

Args:
    server: Read the dedicated server's view instead of the client's.
    target: Address a specific client by name.
    timeout: Seconds for the WHOLE call — the reply and then the dump it
        promises, out of one budget rather than one each. A large world
        takes longer.

Returns counters as integers and the mod's absence markers as null, so a
reading of 0 — a real measurement — cannot be confused with "no data".
Asking both sides at the same moment is the only way to answer "the client
reports no NPC", which one side alone cannot.

\`records` carries the indented list bodies the scalars only summarise:
\`fields["npcs"]` says `active=6 mutated=1`, and `records["npcs"]` says which
six. Those lines were parsed and discarded until now, so a caller could see
that something was there and never what it was.

Input parameters:

- `server` (boolean)
- `target`
- `timeout` (number)

Output parameters:

- `fields` (object)
- `records` (object)
- `side` (string)

### `wait_until` (~494 tokens)

Wait until the game reaches a state

Poll `diag` until one of its fields satisfies a comparison.

Args:
    field: A TOP-LEVEL diag field, exactly as `diag` reports it - `vats`,
        `items`, `world-ready`. Not a path into one: `diag` splits
        `key: value` and stops, so `npcs` is the whole string
        `active=4 mutated=0` and there is no `npcs.active`.
    op: One of `==`, `!=`, `<`, `<=`, `>`, `>=`, `contains`, `changed`.
    value: What to compare against, as text - it is converted to whatever
        type the field actually reads as. Omitted for `changed`, which
        baselines on its first reading.
    server: Watch the dedicated server's view instead of the client's.
    target: Watch a specific client by name.
    timeout: Seconds for the WHOLE call, spent across every poll rather
        than granted to each.
    poll: Seconds between polls.

Use this instead of sleeping and taking a diag. A guessed sleep is wrong in
both directions, and the short one is dangerous: the check reads the state
BEFORE the thing happened, which looks exactly like the feature being
broken.

THE COMPARISON IS TYPED. `diag` returns counters as ints and the
heartbeat's flags as bools; `world-ready == true` compares as a boolean and
\`items >= 10` as a number, so neither `"10" < "9"` nor the truthiness of
\`"False"` can come back here.

IT REFUSES WHAT CAN NEVER COME TRUE rather than waiting it out. An unknown
field names the fields that do exist; ordering a composite string says what
the value actually is. Both used to be spellable and would have reported a
timeout - blaming a game that was answering perfectly.

Not matching is an ANSWER, not an error: it returns `matched: false` with
the last reading it took, so a wait that expected nothing to happen is as
expressible as one that expected something to.

Input parameters:

- `field` (string, required)
- `op` (string, required)
- `poll` (number)
- `server` (boolean)
- `target`
- `timeout` (number)
- `value`

Output parameters:

- `elapsed` (number)
- `last`
- `matched` (boolean)
- `note`
- `polls` (integer)

### `shot` (~247 tokens)

Photograph part of the frame

Capture a region of the game's own back buffer and return the PNG path.

Args:
    region: topleft, topright, bottomleft, bottomright, or full. REQUIRED
        and deliberately without a default — the frame holds the player's
        character name, world name and any chat, so a request says which
        corner it wants.
    target: Address a specific client.
    timeout: Seconds for the WHOLE call — the reply and then the PNG,
        out of one budget rather than one each.

This reads what the game rendered, not the screen, so no other window can
appear in it — by construction rather than by luck. It also sees things the
in-game capture camera cannot: dust and the interface layer.

The path comes back only once the bytes behind it are a WHOLE PNG. A file
exists from the moment it is created rather than the moment it is finished,
so waiting on the name alone would hand back half a picture as readily as a
whole one — and anything else that landed on that name as readily as either.

Input parameters:

- `region` (string, required)
- `target`
- `timeout` (number)

Output parameters:

- `path` (string)
- `region` (string)

### `captures` (~46 tokens)

List the captures on disk

Every capture in the save directory, newest last.

Names, not paths — a path handed out is a path that can come back changed,
and `read_capture` deliberately accepts only a name.

### `read_capture` (~158 tokens)

Read a capture back as an image

Return one capture's PNG as image content.

\`shot` answers with a filesystem path, which is worth nothing to an agent
that is not running on this machine. This is how the picture itself gets
back, and it is a SEPARATE call on purpose: a full-frame PNG is tens of
kilobytes before base64, so a caller that only wanted to know the capture
succeeded should not be made to pay for the pixels.

Args:
    name: A capture filename from `captures`, e.g.
        `<mod>-shot-<token>-001-topleft.png`. A NAME, never a path — see
        `captures.read` for why the containment is structural.

Input parameters:

- `name` (string, required)

### `prune_captures` (~295 tokens)

Delete old captures

Delete all but the newest `keep` captures, and say which went.

Captures accumulated forever. `shot` writes one per call and nothing ever
removed them, so an agent photographing in a loop grew the SAVE DIRECTORY
without bound — the folder holding the worlds and characters, which is not
a cache and not somewhere to leave litter.

Args:
    keep: How many of the newest captures to keep. REQUIRED and
        deliberately without a default, the same way `shot` requires a
        region: this deletes files, and a destructive tool that runs with
        no arguments is one that gets called by accident. `keep=0` removes
        all of them, which is a real request and has to be spelled out.

Only files matching THIS mod's capture pattern are touched, and each is
re-checked to resolve to a direct child of the save directory — the same
containment `read_capture` uses, because a delete that listed and removed
through different rules would be looser than the read beside it. The whole
set is validated before anything is unlinked, so a refusal costs nothing
rather than leaving a half-finished prune.

Newest is decided by MTIME, not by the index in the filename: the index is
this harness's counter and the timestamp is the disk's account of what
happened.

Input parameters:

- `keep` (integer, required)

Output parameters:

- `remaining` (array)
- `removed` (array)

### `status` (~123 tokens)

Is a session running?

Whether a session is running, and what it is.

The only read-only way to ask. Without it an agent that lost track had to
provoke an error to find out — `launch` fails when one exists, `diag` fails
when one does not — so the cheapest question on the surface was the one
that had to be asked by breaking something.

Reports what this server BELIEVES it started. It does not re-query the
process table, so it cannot tell you a game was closed from outside; `stop`
is what verifies against reality.

Output parameters:

- `joined`
- `mode`
- `player`
- `port`
- `running` (boolean)
- `started_pids`
- `world`

### `logs` (~314 tokens)

Read a side's log

Tail one of tModLoader's logs, optionally filtered.

Args:
    name: Which log — see `log_files` for what this install actually has.
        `client.log` and `server.log` are the game; `Launch.log` and the
        `environment-*.log` pair are written by the launcher, which is where
        a run that died BEFORE the game started says why.
    previous: Read the run BEFORE this one. tModLoader zips the previous
        run's logs into `Old/` when a new run starts, so after a failed
        launch and a retry the failure is in an archive and the live log
        belongs to the retry.
    contains: Keep only lines containing this substring, case-insensitively.
        Applied to the WHOLE log before the tail, so this returns the last N
        MATCHING lines rather than the matches among the last N.
    lines: How many trailing lines to return. Zero returns none; a negative
        count is refused rather than guessed at.

Useful when a launch fails: the reason is usually in a log and not in
anything the trigger protocol can reach, because the game never got far
enough to poll. Which is also why `previous` exists — the obvious thing to
do after a failed launch is launch again, and that rotates the evidence.

Input parameters:

- `contains`
- `lines` (integer)
- `name` (string)
- `previous` (boolean)

### `log_files` (~56 tokens)

List the logs this install has

Which logs exist right now, and how many earlier runs are archived.

Read off disk rather than listed as a constant: which logs exist depends on
what was run, and a server-only session writes no `client.log` at all.

### `inventory` (~276 tokens)

What this install has

The worlds, characters and mods on this machine.

\`launch` states two preconditions and could check neither. `player` must
already exist — it does not create one, and a duplicate is kicked — and
\`world` wants a WINDOWS path the caller had to know in advance. Both are
facts about directories sitting right there, and until now the only way to
learn either was to launch and read the failure: a kick for the wrong
character, and a readiness timeout blaming the heartbeat for the wrong
world. Each world's `path_win` is the exact string `launch(world=...)`
wants.

The mods answer something else. `commands` reports `responder: false` for
three different situations — the mod is not built, or it is built and
switched off, or it is on and was compiled without the dev bridge — and
\`enabled` plus `built_here` separate the first two.

THOSE TWO ARE NOT ONE FACT. A mod can be enabled and have no `.tmod` here,
because a workshop mod is installed from somewhere else entirely; on the
install this was written against, `CheatSheet` is exactly that. Collapsing
them into `installed` would report it missing and send someone rebuilding a
mod that was never the problem.

Output parameters:

- `mods` (array)
- `players` (array)
- `worlds` (array)

### `save_snapshot` (~251 tokens)

Copy the save aside

Copy this world and its characters aside, so a run can be undone.

WHAT THIS IS FOR. The mutating verbs write to a real install and none of
that FAILS — it accumulates. Enemy NPCs do not survive a reload, so `spawn`
looks harmless, but `give` writes the character file, `time` and `weather`
live in the world, and `settile` changes it for good. The damage is
invisible when it is done and shows up later as a measurement nobody
doubts.

Take one before a run that mutates, and `save_restore` after it.

REFUSES WHILE THE GAME IS RUNNING, naming the pids. A running tModLoader
owns these files and writes them out on its own schedule, so a copy taken
now is mid-write. Stop the session first.

Copies the configured world's `.wld` and `.twld` and every `.plr`/`.tplr` —
not the whole Worlds directory, which measured 41MB against 3MB for one
world, and not `.bak` files, which are the game's own safety net.

Input parameters:

- `label` (string, required)

Output parameters:

- `files` (array)
- `label` (string)
- `size` (integer)
- `taken` (number)

### `save_restore` (~113 tokens)

Put a saved copy back

Overwrite the world and characters with a snapshot.

This DESTROYS what is on disk now, so it saves that first: the state being
overwritten is copied to `auto-before-restore` and returned as `undo`,
which `save_restore` accepts like any other label. A restore aimed at the
wrong snapshot is therefore recoverable rather than final.

Refuses while the game is running, and refuses a label that does not exist
by listing the ones that do.

Input parameters:

- `label` (string, required)

Output parameters:

- `files` (array)
- `label` (string)
- `removed` (array)
- `size` (integer)
- `undo`

### `save_snapshots` (~65 tokens)

List the saved copies

Every snapshot on this machine, newest first, with its age in seconds.

A snapshot whose manifest cannot be read is omitted rather than listed as
empty, because `save_restore` refuses it for the same reason: putting back
nothing and reporting success is the worst available outcome.

Output parameters:

- `root` (string)
- `snapshots` (array)

### `heartbeat` (~422 tokens)

Read the mod's heartbeat

Why the game is not answering, for both sides at once.

\`launch` already reads this file to decide readiness and keeps one bit of
it. When a launch SUCCEEDS that is all anyone needs. When it fails, the
discarded detail is the entire answer, and what comes back instead is `no
live heartbeat within 300s` — which names the symptom and none of the four
causes:

\- **absent** — nothing ever wrote one. The mod is not loaded, is not
  enabled in this install, or was built without the dev bridge. For
  clients this is an EMPTY `clients` list, not an entry saying so.
\- **stale** — a game ran and is no longer running. The file outlives the
  process, so this is indistinguishable from live to anything that only
  checks whether it exists.
\- **live, no world** — still loading. Nothing is wrong; wait longer.
\- **live, world, not armed** — loaded and ticking, bridge not listening.

\`clients` is a LIST because two clients can share one save directory and
the old single-client shape reported whichever wrote last. Expect an entry
per client — and one more: an entry with `player: null` is the untokened
heartbeat every client writes before its character loads, which nothing
deletes and each new client overwrites. Treat it as a slot rather than as
a client. A real client is the one carrying a token and an advancing
\`polls`.

Reads OFF DISK and needs no session, deliberately: a failed `launch` raises
without storing one, so a tool that required a session could never answer
the question it exists for. It is also the only tool here that is useful
when nothing else is.

Both sides are returned together because "the client is silent and the
server is fine" is a different diagnosis from both being silent, and asking
one at a time cannot see the difference.

Output parameters:

- `clients` (array)
- `server`

### `log_since` (~372 tokens)

Read what a log has gained

Only what a log has gained since you last looked.

Args:
    name: A log filename from `log_files`.
    offset: The `next_offset` from your previous call, or 0 to start at the
        beginning. BYTES, not lines — a line count is not a resume point,
        because the number of lines you have read is not where the file
        continues.
    contains: Case-insensitive filter, applied to the new lines only.
    fingerprint: The previous call's `fingerprint`, or omit on the first.
        It is how a rotation is detected when the NEW log has already
        outgrown your offset — without it that case reads as a quiet
        continuation, silently skipping the head of the new run.

NOT A LIVE TAIL, and it cannot be one. Tools here are synchronous and a
game session is process-global state, so a `launch` blocking for five
minutes is not something another call watches from the side. What this
buys is the read between calls: `logs` re-reads a file that grows all run,
and this returns the new part.

\`restarted` is the field to check. tModLoader ZIPS the previous run's logs
and starts fresh, so an offset from a run that has since rotated points
past the end of a now-shorter file. Reading there would report an empty log
forever, which looks exactly like a quiet game rather than like a log that
restarted underneath you. When that happens the read begins again at zero
and says so, because handing back the whole file is only correct if the
caller is told why.

Input parameters:

- `contains`
- `fingerprint`
- `name` (string, required)
- `offset` (integer)

Output parameters:

- `fingerprint` (string)
- `lines` (array)
- `next_offset` (integer)
- `restarted` (boolean)
- `truncated` (boolean)

### `api_search` (~321 tokens)

Search the tModLoader API surface

Find a type, field, property or method in the INSTALLED tModLoader.

Args:
    query: Part of a name, or a type. `cloudAlpha`, `rain`, `QuickSpawnItem`,
        `IEntitySource`. Case-insensitive.
    kind: Narrow to one of type, field, property, method.
    limit: How many matches to return, best first.

ANSWERS THE QUESTION YOU HAVE BEFORE YOU WRITE ANYTHING. A compile tells you
exactly whether the call you already wrote is right; it cannot tell you what
is there. `Main.maxRaining` is only findable if you already suspect the name.

READ FROM THE ASSEMBLY'S OWN METADATA, so it cannot drift from the version
installed — which is the failure mode of every wiki page and every model's
recollection of an API. It carries no prose, because it is not documentation:
it is the public surface, with signatures.

The index is built once per tModLoader version and cached against the DLL it
came from, so a game update invalidates it by construction rather than by
anybody remembering to. The first call after an update pays a few seconds.

Needs a .NET SDK, because the indexer is a small C# tool — it reads metadata
without loading or running the game assembly. Without one this refuses and
says so, rather than answering from a stale or absent index.

Input parameters:

- `kind`
- `limit` (integer)
- `query` (string, required)

Output parameters:

- `indexed` (integer)
- `matches` (array)
- `truncated` (boolean)

### `log_watch` (~444 tokens)

Wait for a line to appear in a log

Block until a log line matches, instead of polling `log_since` by hand.

Args:
    name: A log filename from `log_files`.
    contains: Case-insensitive text to wait for. REQUIRED — without one
        this matches the first line written and is `log_since` wearing a
        longer name.
    offset: Where to start reading. 0 includes the log's HISTORY, which is
        usually what you want ("did the mod load" is a question about a
        line that is already there). Pass a previous call's `next_offset`
        to watch only what comes after it.
    fingerprint: The `fingerprint` from an earlier `log_since`/`log_watch`,
        when resuming — see `log_since` for the rotation it catches.
        Within one call the polls carry it themselves.
    timeout: Seconds for the WHOLE call, spent across every poll.
    poll: Seconds between reads.

THE OFFSET IS THE MECHANISM. Each poll resumes where the last stopped, so a
line is matched exactly once — never missed in the gap between two polls,
and never re-reported on the next. A watch that re-read the file from the
top would match a line written before the wait began and call it news,
which is how "wait for the crash" passes on the crash from the PREVIOUS run.

Not matching is an ANSWER, not an error: it returns `matched: false` with
the resume point, so "nothing was logged for 30s" is as expressible as
waiting for something. A MISSING log still raises, because that is nobody
having been asked rather than a line failing to arrive.

\`restarted` means the log rotated during the wait — tModLoader zips the
previous run's logs and starts fresh, so your offset stopped meaning
anything and the lines you are holding came out of a different file.

Input parameters:

- `contains` (string, required)
- `fingerprint`
- `name` (string, required)
- `offset` (integer)
- `poll` (number)
- `timeout` (number)

Output parameters:

- `elapsed` (number)
- `fingerprint` (string)
- `lines` (array)
- `matched` (boolean)
- `next_offset` (integer)
- `polls` (integer)
- `restarted` (boolean)

### `restart` (~269 tokens)

Rebuild and relaunch the session

Stop, rebuild, and start again with the session's own settings.

Args:
    build: Compile the mod between stopping and starting. On by default,
        because picking up a code change is the reason this loop exists.
    timeout: Seconds to wait for readiness on the relaunch.
    build_timeout: Seconds to allow the compile.

THE ORDER IS THE POINT. tModLoader REFUSES to build while the game is open
and reports it with an error that reads like a compile failure, so
stop-then-build-then-launch is not a preference — building first sends you
hunting a syntax error that is not there. Three separate calls let a caller
get that order wrong; this one cannot.

The mode, port, player and WORLD come from the running session rather than
from arguments or defaults. That last one is why `Session` had to start
recording the world it resolved: a relaunch that fell back to the
configured default would quietly load a different world than the one being
tested, and report success.

Needs a running session, because a session is where those settings live.
With nothing running there is nothing to reuse — call `launch`.

Input parameters:

- `build` (boolean)
- `build_timeout` (number)
- `timeout` (number)

Output parameters:

- `build_summary`
- `built`
- `killed_pids` (array)
- `mode` (string)
- `player` (string)
- `port` (integer)
- `started_pids` (array)
- `world`

### `stop` (~222 tokens)

Stop the game session

Kill only the processes this session started, and confirm they are gone.

Args:
    settle: Seconds a killed process may take to leave the process table
        before it counts as a survivor. `/F` returns before Windows has
        caught up, so verifying too eagerly reports a successful teardown as
        a refused one — raise this on a loaded machine rather than lower it.

Surgical on purpose: a developer usually has their own game open, and a
teardown that killed every tModLoader it could find would take it with them.

\`killed_pids` are pids VERIFIED to have left the process table, not pids a
kill was aimed at. If any survive, this FAILS rather than answering with a
shorter list — and the session is deliberately kept, so calling `stop`
again retries exactly those pids. Releasing it would leave a running game
that nothing owns, which is how the next `launch` ends up refusing to start
over a process nobody remembers starting.

Input parameters:

- `settle` (number)

Output parameters:

- `killed_pids` (array)
- `note`

## Diagnostics

Captured diagnostic sections: Provenance, Install scripts, Dependencies. The full working is on the page: https://verifymcp.io/servers/musharna-tmodloader-mcp/tmodloader-mcp#diagnostics

## Score history

- 2026-09-20: 54
- 2026-09-19: 54
- 2026-09-18: 54
- 2026-09-17: 54

## Common questions

### What is the tmodloader-mcp server?

tmodloader-mcp is listed in the public MCP registry as io.github.musharna/tmodloader-mcp. Drive a running tModLoader (Terraria) instance from an agent: launch, query, screenshot, read state. This page covers its PyPI package (tmodloader-mcp).

### Is the tmodloader-mcp server safe to use?

tmodloader-mcp scores 54 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 20 September 2026. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the tmodloader-mcp server expose?

tmodloader-mcp exposes 24 tools: build_mod, launch, join, trigger, commands, and 19 more. Their descriptions and schemas cost roughly 6,270 tokens of context every time the server is loaded.

### Is the tmodloader-mcp server still maintained?

tmodloader-mcp is still listed as active in the MCP registry. We last reached this channel on 20 September 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the tmodloader-mcp server under?

tmodloader-mcp declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- PyPI project: https://pypi.org/project/tmodloader-mcp/
- Socket report: https://socket.dev/pypi/package/tmodloader-mcp
- Repository: https://github.com/musharna/tmodloader-mcp
- Changelog RSS feed: https://verifymcp.io/servers/musharna-tmodloader-mcp/tmodloader-mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/musharna-tmodloader-mcp/tmodloader-mcp.json
- HTML version of this page: https://verifymcp.io/servers/musharna-tmodloader-mcp/tmodloader-mcp
