An OpenCode V2 plugin that lets an AI agent monitor and operate local Minecraft servers (Paper / Fabric / vanilla) running on the same Linux box.
It exposes five tools in the mc namespace:
| Tool | Id | Purpose |
|---|---|---|
status |
mc_status |
Running state, uptime, memory, version, players, TPS, recent errors |
logs |
mc_logs |
Tail and filter logs/latest.log, highlighting errors |
backup |
mc_backup |
Newest backup archive + systemd backup timer state |
cmd |
mc_cmd |
Run a console command over RCON (allowlist + confirmation gated) |
restart |
mc_restart |
Restart a server through its systemd unit (double gated) |
The plugin has zero runtime dependencies and never prints the RCON password.
Add the plugin to opencode.jsonc (global ~/.config/opencode/opencode.jsonc or a project one):
Restart OpenCode (or reload the plugin) so the tools are registered.
Once the package is published:
{
"plugins": [
{
"package": "opencode-mc-ops",
"options": { "servers": [ /* … */ ] }
}
]
}| Option | Type | Default | Description |
|---|---|---|---|
servers |
ServerConfig[] |
[] |
The servers to manage. See below. |
allowedCommands |
string[] |
list, save-all, save-off, save-on, tps, whitelist, seed, time, weather, difficulty, gc |
Console commands that may be executed. Anything else is refused. |
dangerousCommands |
string[] |
stop, restart, op, deop, ban, pardon, whitelist off, kick, end, save-all flush |
Commands that additionally require confirm: true. |
allowCommands |
boolean |
true |
Master switch for mc_cmd. When false every command is refused. |
allowRestart |
boolean |
false |
Master switch for mc_restart. |
rconHost |
string |
127.0.0.1 |
Default RCON host. |
rconPort |
number |
25575 |
Default RCON port (used when server.properties has no rcon.port). |
rconPassword |
string |
"" |
Default RCON password. Empty means "read it from server.properties". |
rconTimeoutMs |
number |
5000 |
RCON connect/command timeout. |
useSudo |
boolean |
false |
Run the restart through sudo -n systemctl restart …. |
restartCommand |
string |
null |
Custom restart command template, e.g. "sudo -n systemctl restart {unit}". {unit} is replaced by the unit name. |
backupDirNames |
string[] |
["backups", "backup"] |
Directory names searched inside a server directory for backups. |
| Field | Type | Description |
|---|---|---|
name |
string |
Display name, used to select the server in every tool. |
dir |
string |
Absolute path of the server directory. |
type |
"paper" | "fabric" | "vanilla" | "other" |
Server implementation. Used for hints only. |
service |
string |
systemd unit that runs the server, e.g. zenith-server.service. Enables running state, uptime, memory and restart. |
backupTimer |
string |
systemd timer that drives the backup job, e.g. mc-backup.timer. |
backupDirs |
string[] |
Explicit backup directories. Defaults to <dir>/backups and <dir>/backup. |
logFile |
string |
Log file to read. Defaults to <dir>/logs/latest.log. |
rcon.host / rcon.port / rcon.password |
string / number / string |
Per-server RCON overrides. |
rconHost / rconPort / rconPassword |
string / number / string |
Flat aliases for the same settings. |
RCON password resolution — the password is looked up in this order and the first non-empty value wins:
servers[].rcon.passwordservers[].rconPassword- top level
rconPassword rcon.passwordin<dir>/server.properties← the normal case
The resolved password is used for the connection only. It is never logged, never echoed in tool output and never included in error messages.
Input
{
"server": "SchoolServer" // optional: omit to inspect every configured server
}Output
SchoolServer (paper) — RUNNING
service: zenith-server.service (active/running, since Mon 2026-10-20 09:17:55 EEST, up 2h 26min)
memory: 4.21 GiB
version: Paper 26.2-84-main@26e81c4 (Minecraft 26.2)
players: 3/120 online (live rcon)
tps: 20.0 (1m), 20.0 (5m), 20.0 (15m) (rcon)
rcon: 127.0.0.1:25575 (reachable)
recent errors (1):
- [Train_Carts] Failed to save offline group data to disk (x29)
java.nio.file.FileSystemException: plugins/Train_Carts/trains.groupdata.tmp: Invalid argument
When a server is stopped the tool says so explicitly, shows the stop reason (or the last error) and the last log lines:
MechanicsCubed (fabric) — STOPPED
service: minecraft.service (inactive/dead)
memory: unknown
version: Fabric 26.2 (Loader 0.19.5)
players: unknown (max 20)
tps: unknown
rcon: disabled (enable-rcon=false in server.properties)
stop: server was stopped — [10:06:24] [Server thread/INFO]: Stopping server
last log lines:
[10:06:24] [C2ME Storage #2/INFO]: Storage thread Thread[#153,C2ME Storage #2,8,main] stopped
Data sources:
- running state —
systemctl show -p ActiveState -p SubState - uptime —
systemctl show -p ActiveEnterTimestamp(plus the monotonic clock as fallback) - memory —
systemctl show -p MemoryCurrent - version — parsed from the log banner (
This server is running Paper version …,Loading Minecraft … with Fabric Loader …,Starting minecraft server version …); whenlogs/latest.logwas rotated,server.log/startup.logare checked as well - players — live RCON
list, falling back to the lastThere are N of a max of M players online:log line - TPS — live RCON
tps, falling back to the lastTPS from last 1m, 5m, 15m:log line - max players —
max-playersfromserver.properties - recent errors — the last distinct
ERROR/FATAL/ exception messages, repeated occurrences collapsed into anxNcounter
Input
{
"server": "SchoolServer", // optional
"grep": "Train_Carts", // optional regular expression (case insensitive)
"lines": 200 // optional, default 200, max 5000
}Output
SchoolServer — matching /Train_Carts/ of /mnt/GamesProjects/servers/SchoolServer/logs/latest.log
scanned 20000 lines, 612 matched, 500 shown, 500 error lines
(output truncated — showing the most recent matches only)
!! [15:10:41] [TrainCarts-OfflineGroupSaver/ERROR]: [Train_Carts] Failed to save offline group data to disk
!! java.nio.file.FileSystemException: plugins/Train_Carts/trains.groupdata.tmp: Invalid argument
Lines that look like an error are prefixed with !!. An invalid regular expression is reported instead of throwing.
Input
{
"server": "SchoolServer" // optional
}Output
SchoolServer — backups
directories: /mnt/GamesProjects/servers/SchoolServer/backups
newest: zenith_backup_20261007_000000.tar.gz
path: /mnt/GamesProjects/servers/SchoolServer/backups/zenith_backup_20261007_000000.tar.gz
size: 17.9 GiB
age: 13d 11h 53min ago
files: 2 found (1 archives)
timer: mc-backup.timer (active)
last run: Tue 2026-10-20 09:17:55 EEST [success]
next run: Wed 2026-10-21 00:00:00 EEST
The timer information comes from systemctl show -p LastTriggerUSec -p NextElapseUSecRealtime -p Result.
Input
{
"server": "SchoolServer",
"command": "list",
"confirm": false // only needed for dangerous commands
}Output
$ list (SchoolServer)
There are 3 of a max of 120 players online: Steve, Alex
Refusals explain exactly what is wrong:
Refusing to run "say hello": it is not on the allowlist. Allowed commands: list, save-all, tps, whitelist, gc.
Refusing to run "whitelist off": it is classified as dangerous. Re-run the tool with confirm: true if you really want to execute it.
If RCON is not reachable:
RCON is unreachable for SchoolServer at 127.0.0.1:25575: connect ECONNREFUSED 127.0.0.1:25575
Input
{
"server": "MechanicsCubed",
"confirm": true
}Output
Restarted minecraft.service (MechanicsCubed). The unit is now active/running.
or, on failure:
Failed to restart minecraft.service (MechanicsCubed): Job for minecraft.service failed because the control process exited with error code.
The plugin is built to be safe to hand to an autonomous agent.
- Allowlist —
mc_cmdonly runs commands whose name (or full form) appears inallowedCommands. Everything else is refused with a message that lists the allowed commands. - Dangerous list — commands in
dangerousCommandsneed an explicitconfirm: trueon the tool call. A bare entry such asstopcovers every command that starts with it; an entry with arguments such aswhitelist offonly covers that exact command (and longer variants of it). - Two gates for dangerous commands — a command has to be allowlisted and confirmed.
stopis refused as long as it is not allowlisted; as soon as it is, the confirmation prompt is required. - Master switches —
allowCommands: falsedisablesmc_cmdcompletely,allowRestart: falsedisablesmc_restartcompletely.allowRestartdefaults tofalse, so a fresh installation cannot restart anything until it is switched on explicitly. - Permission prompts — before a console command that is not known to be read-only (
list,tps,seed,help,version,whitelist) and before every restart, the plugin callsctx.permission.assert(...), so OpenCode can ask the user for approval. Errors are propagated, never swallowed. - Double confirmation for restarts —
mc_restartrequires bothallowRestart: truein the options andconfirm: trueon the call. - Read-only by default —
mc_status,mc_logsandmc_backupnever change anything. - No secret leakage — the RCON password is never printed, never logged and never part of an error message.
- Linux with systemd (
systemctlmust be onPATH). - Node.js ≥ 22 or Bun — the plugin is plain TypeScript and is loaded by the OpenCode runtime.
- A Minecraft server reachable on the local machine, with:
- a
server.propertiesfile (max-players,rcon.*), - a
logs/latest.logfile, - optionally a systemd unit (needed for
mc_statusrunning state and formc_restart).
- a
- RCON for
mc_cmdand for live player/TPS numbers:- Paper:
enable-rcon=true,rcon.port=25575,rcon.password=<secret>. - Fabric has no built-in RCON — enable it with a mod (e.g.
rconmod) or usemc_cmdonly on Paper servers.
- Paper:
- Permission to restart —
systemctl restartnormally needs root. Either run OpenCode as a user that may restart the unit, or enableuseSudo: true(this box has passwordless sudo) or set a customrestartCommand.
zenith-server.service carries a drop-in that stops the unit from starting while the sleep sentinel exists:
# /etc/systemd/system/zenith-server.service.d/sleep.conf
[Unit]
ConditionPathExists=!/mnt/GamesProjects/servers/SchoolServer/.asleepIf the sentinel is present, systemctl restart zenith-server.service succeeds but the unit stays inactive; the plugin reports the resulting state so the agent can explain what happened.
index.ts plugin entry point: registers the five tools in the "mc" namespace
src/core.ts all logic — option parsing, log parsing, command policy, formatting
src/systemd.ts systemctl helpers + server.properties reader
src/rcon.ts minimal Source-RCON client (no dependencies)
types/opencode.d.ts ambient declaration of the OpenCode V2 plugin API
tests/core.test.ts unit tests (node:test)
- No OpenCode imports in the logic.
src/core.tsandsrc/systemd.tsdo not import@opencode/plugin, so they can be unit tested with plainnode --test. Onlyindex.tstouches the plugin API, and it does so withimport type(type-only, erased at runtime). - Bounded I/O. The servers live on an NTFS mount where directory walks are slow, so the plugin never scans a world directory. Logs are read by seeking to the tail of the file and reading a bounded number of bytes (512 KiB normally, 4 MiB when a grep filter is used). Backup directories are only listed one level deep and only the file metadata is read.
- systemd first. Running state, uptime and memory come from
systemctl show(single invocation per server), not from log scraping. - Live data with graceful fallback. RCON is probed with a short timeout. When it is disabled or unreachable the plugin falls back to the log and says which source it used.
- Pure helpers. Parsing (
server.properties, log lines, versions, TPS, player counts) and the command policy are pure functions, which is what the test suite exercises.
npm install # installs the dev dependencies (typescript, @types/node)
npm run typecheck # tsc --noEmit
npm test # node --test tests/The test suite is hermetic: it uses inline fixtures and never touches the network or a real server.
MIT — Copyright (c) 2026 winmasttdev
{ "$schema": "https://opencode.ai/config.json", "plugins": [ { "package": "file:///home/winmastt/dev/opencode-mc-ops", "options": { "servers": [ { "name": "SchoolServer", "dir": "/mnt/GamesProjects/servers/SchoolServer", "type": "paper", "service": "zenith-server.service", "backupTimer": "mc-backup.timer" }, { "name": "MechanicsCubed", "dir": "/mnt/GamesProjects/servers/MechanicsCubed", "type": "fabric", "service": "minecraft.service", "backupTimer": "mc-mechanics-backup.timer" } ], "allowedCommands": ["list", "save-all", "save-off", "save-on", "tps", "whitelist", "seed", "time", "weather", "difficulty", "gc"], "dangerousCommands": ["stop", "restart", "op", "deop", "ban", "pardon", "whitelist off", "kick", "end", "save-all flush"], "allowRestart": true, "allowCommands": true } } ] }