The --experimental-bun flag enables a TypeScript/JavaScript scripting sidecar powered by Bun. It runs alongside the Minecraft server, tails the server log for events, and connects via RCON to send commands.
Performance Note: Bun 1.3+ is recommended for optimal performance. Recent versions include significant improvements:
- structuredClone 25x faster for arrays
- Buffer.slice() 1.8x faster
- path.parse() up to 7x faster
- Updated JavaScriptCore engine
- Improved bundling and barrel import optimization
The installer automatically detects your Bun version and upgrades if necessary.
mc-dad-server install --experimental-bunScripts live in ~/minecraft-server/bun-scripts/scripts/. Drop a .ts or .js file there and restart the server.
// scripts/welcome.ts
declare const mc: import("../runtime/server").McServer;
mc.on("playerJoin", async (e) => {
await mc.say(`Welcome, ${e.player}!`);
});An example script is deployed on first install. See bun-scripts/scripts/example.ts for a full demo.
Listen for server events with mc.on():
| Event | Payload | When |
|---|---|---|
playerJoin |
{ player, timestamp } |
Player joins |
playerLeave |
{ player, timestamp } |
Player leaves |
chat |
{ player, message, timestamp } |
Chat message sent |
playerDeath |
{ player, message, timestamp } |
Player dies |
playerAdvancement |
{ player, advancement, timestamp } |
Advancement earned |
serverStart |
{ timestamp } |
Server finishes starting |
serverStop |
{ timestamp } |
Server begins stopping |
rconReady |
{ timestamp } |
RCON connection established |
await mc.command("time set day"); // raw RCON command
await mc.say("Hello everyone!"); // broadcast message
await mc.kick("player", "reason"); // kick a player
await mc.tp("player", 0, 64, 0); // teleportAll commands flow through the command filter and rate limiter (see Security below).
// Run every 30 minutes
mc.scheduler.every(30 * 60_000, async () => {
if (mc.players.count > 0) {
await mc.say("Remember to save your builds!");
}
});
// Run once after 10 seconds
const task = mc.scheduler.after(10_000, () => {
console.log("Delayed task ran");
});
// Cancel
task.cancel();mc.players.online; // PlayerInfo[] — currently connected
mc.players.count; // number
mc.players.isOnline("Steve"); // booleanExpose HTTP endpoints for external integrations (CI, Discord bots, dashboards):
mc.webhooks.addRoute({
path: "/api/say",
method: "POST",
handler: async (req) => {
const { message } = await req.json();
await mc.say(message);
return Response.json({ ok: true });
},
});
mc.webhooks.start(9090);The webhook server binds to 127.0.0.1 by default. See Configuration for overrides.
User scripts can import npm packages. Add dependencies to bun-scripts/package.json:
cd ~/minecraft-server/bun-scripts
bun add zodThen import normally in your scripts:
import { z } from "zod";For private registries (Azure Artifacts, GitHub Packages, Artifactory), add a .npmrc or bunfig.toml in the bun-scripts/ directory.
The sidecar includes layered security hardening. All features are on by default and configurable via environment variables.
Dangerous RCON commands are blocked by default:
op, deop, stop, ban, ban-ip, pardon, pardon-ip, whitelist, save-off, save-all, save-on
Blocked attempts are logged: [mc-scripts] Blocked RCON command: ...
Scripts can adjust the filter at runtime:
mc.commandFilter.block("give");
mc.commandFilter.unblock("save-all");
mc.commandFilter.setBlocklist(["op", "deop", "stop"]);RCON commands are rate-limited with a token bucket (default: 20/s, burst 40). Excess commands are dropped with a warning.
The webhook server binds to 127.0.0.1 by default (not 0.0.0.0). Privileged ports (< 1024) are rejected. Non-localhost binding logs a warning.
The script loader rejects filenames containing .., /, or \ and verifies resolved paths stay within the scripts/ directory.
On first load, a .manifest.json is generated with SHA-256 hashes of all scripts. On subsequent loads, modified scripts trigger a warning:
[mc-scripts] WARNING: Script modified since last manifest: my-script.ts
Regenerate the manifest after intentional changes:
cd ~/minecraft-server/bun-scripts
bun run runtime/index.ts --rehashThe Bun process runs in a subshell with ulimit constraints:
- 512 MB virtual memory
- 256 max open file descriptors
These limits only affect the scripting sidecar, not the Java server.
The RCON password is never written to disk in the sidecar's .env file. It is extracted at runtime from server.properties and passed as an inline environment variable to the Bun process.
Container mode: When running in
--mode container, the Go CLI also reads the RCON password fromserver.propertiesfor its own RCON client. The sidecar's RCON and the CLI's RCON use the same credentials and protocol.
All settings are configurable via environment variables in bun-scripts/.env:
| Variable | Default | Description |
|---|---|---|
RCON_PORT |
25575 |
RCON port |
RCON_HOST |
127.0.0.1 |
RCON host |
MC_SERVER_DIR |
Server install dir | Path to Minecraft server |
RCON_BLOCKED_COMMANDS |
See blocklist | Comma-separated blocked commands (empty = allow all) |
RCON_RATE_LIMIT |
20 |
Max RCON commands per second |
RCON_RATE_BURST |
40 |
Rate limiter burst capacity |
WEBHOOK_HOST |
127.0.0.1 |
Webhook bind address |
WEBHOOK_PORT |
9090 |
Webhook port (overrides script-provided value) |
Environment variables set in the shell take precedence over .env file values (Bun built-in behavior).
~/minecraft-server/bun-scripts/
.env # Configuration (auto-generated)
package.json # Dependencies
tsconfig.json # TypeScript config
runtime/ # Framework (overwritten on upgrade)
index.ts # Sidecar bootstrap
server.ts # McServer API
command-filter.ts # RCON command blocklist
rate-limiter.ts # Token bucket rate limiter
integrity.ts # Script hash verification
webhooks.ts # HTTP webhook server
events.ts # Typed event bus
rcon.ts # RCON protocol client
log-parser.ts # Minecraft log parser
players.ts # Online player tracker
scheduler.ts # Task scheduling
types.ts # Type definitions
scripts/ # Your scripts (preserved across upgrades)
example.ts # Example script