How to build a Discord bot showing your MU server status
Build a Discord bot that displays online/offline status, connected players, and rankings for your MU Online server, with automatic updates and slash commands.
A Discord bot that shows your MU Online server status in real time is one of the highest return-on-effort community tools you can build. While a player is chatting on Discord, they can see at a glance whether the server is up, how many people are connected, and who leads the reset ranking — without
A Discord bot that shows your MU Online server status in real time is one of the highest return-on-effort community tools you can build. While a player is chatting on Discord, they can see at a glance whether the server is up, how many people are connected, and who leads the reset ranking — without opening the client or the website. That drastically cuts the repeated "is it on?" questions in your channels and projects the image of a well-run, professional server. In this tutorial you will build, from scratch, a Node.js bot using the discord.js library that queries the server state and publishes a panel that updates itself, and also responds to slash commands such as /status and /online. We will cover both the path of reading the database directly and the safer path of consuming an API on the site itself. Keep in mind from the start: table names, columns, and ports vary by version and emulator (Season 6, Season 15, IGCN, MuEMU, etc.), so the examples here are a skeleton to adapt, not universal truth.
Prerequisites
Before writing a single line of code, make sure the base environment is ready. A bot is just one more client consuming data that already exists; if the data source is not accessible and stable, no amount of code will save you. If you are still building the infrastructure, it is worth reviewing the how to create an MU Online server guide first, because the bot assumes a working server.
- Node.js 18 LTS or higher installed on the machine where the bot will run (discord.js v14 requires at least Node 16.11).
- Access to the server's database (SQL Server in most versions) or a status API on the site.
- A Discord Developer Portal account with permission to create applications.
- A Discord server where you have administrator permission to add the bot.
- Basic command-line and config-file editing knowledge.
- Firewall configured: if you will read the database from another machine, the SQL Server port (e.g., 1433) must be open only to the bot's IP.
Architecture: direct database vs. site API
There are two models for the bot to obtain the data, and the choice defines the entire security of the project.
| Approach | Advantages | Disadvantages | When to use |
|---|---|---|---|
| Read the database directly | Simple, no extra layer, raw data | Exposes the SQL port on the network; risky if the token/credential leaks | Bot runs on the same VPS as the database, closed network |
| Consume site API | You control exactly what goes out; the database stays closed | Requires creating and maintaining an endpoint on the site | Bot runs on an external machine; production environment |
The recommendation for production is the site API: you create an endpoint such as /api/status.php that returns a lean JSON, and the bot only sees that. This way, even if the bot token leaks, no one reaches the database. We will show both, but treat the API as the preferred path.
Step 1 — Create the application and bot on Discord
- Go to the Discord Developer Portal and click New Application. Give it a name like
StatusMU. - In the side menu, go to Bot and click Add Bot.
- Under Privileged Gateway Intents, for this status bot you usually do not need privileged intents, which simplifies approval. Leave them off if you are only publishing embeds.
- Click Reset Token, copy the token, and store it carefully — it will be used in the
.env. - Go to OAuth2 → URL Generator, check the
botandapplications.commandsscopes, and in the permissions check Send Messages, Embed Links, and Read Message History. - Copy the generated URL, open it in your browser, and add the bot to your Discord server.
Step 2 — Project structure
In the terminal on the bot's machine, create the folder and install the dependencies:
mkdir statusmu && cd statusmu
npm init -y
npm install discord.js dotenv mssql axios
Create a .env file at the project root. This file must never go into Git:
DISCORD_TOKEN=paste_your_token_here
CLIENT_ID=application_id
GUILD_ID=your_discord_server_id
STATUS_CHANNEL_ID=status_channel_id
# Option A: direct database
DB_SERVER=127.0.0.1
DB_PORT=1433
DB_DATABASE=MuOnline
DB_USER=mubot_reader
DB_PASSWORD=StrongPassword!123
# Option B: site API
STATUS_API_URL=https://yoursite.com/api/status.php
Step 3 (option A) — Status endpoint on the site
If you choose the API — the safe path — create a PHP file on the site that returns the status as JSON. It encapsulates all the query logic and never exposes credentials to the bot:
<?php
// api/status.php
header('Content-Type: application/json');
$host = '127.0.0.1';
$db = 'MuOnline';
$user = 'site_reader';
$pass = 'SitePassword!123';
try {
$conn = new PDO("sqlsrv:Server=$host;Database=$db", $user, $pass);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Connected players — table name varies by version
$stmt = $conn->query("SELECT COUNT(*) AS online
FROM MEMB_STAT WHERE ConnectStat = 1");
$online = (int) $stmt->fetch(PDO::FETCH_ASSOC)['online'];
echo json_encode([
'status' => 'online',
'online' => $online,
'updated' => date('c'),
]);
} catch (Exception $e) {
echo json_encode(['status' => 'offline', 'online' => 0]);
}
Test by opening the URL in your browser — you should see clean JSON. The big advantage: the bot only knows this URL, and you decide exactly which fields appear.
Step 4 — Actually test whether the server is up
The classic mistake is treating the server as "online" just because the database responded. The database stays up even when the GameServer is stopped. To reflect the real state, also run a TCP port test against the GameServer. In Node.js:
// checkPort.js
const net = require('net');
function checkPort(host, port, timeout = 3000) {
return new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.once('connect', () => { socket.destroy(); resolve(true); });
socket.once('timeout', () => { socket.destroy(); resolve(false); });
socket.once('error', () => { resolve(false); });
socket.connect(port, host);
});
}
module.exports = { checkPort };
Use your GameServer's port (often 55901 or 44405, but it varies by version). Only mark it as online when the port responds AND the data query works.
Step 5 — The main bot with an automatic panel
Create index.js. It queries the status every 60 seconds and edits a single fixed message in the channel, keeping the panel always up to date without cluttering the chat:
require('dotenv').config();
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
const axios = require('axios');
const { checkPort } = require('./checkPort');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
let statusMessageId = null;
async function fetchStatus() {
try {
const gameOnline = await checkPort('127.0.0.1', 55901);
const { data } = await axios.get(process.env.STATUS_API_URL, { timeout: 5000 });
return {
online: gameOnline && data.status === 'online',
players: data.online ?? 0,
};
} catch (err) {
return { online: false, players: 0 };
}
}
function buildEmbed(status) {
return new EmbedBuilder()
.setTitle('Server Status')
.setColor(status.online ? 0x2ecc71 : 0xe74c3c)
.addFields(
{ name: 'State', value: status.online ? '🟢 Online' : '🔴 Offline', inline: true },
{ name: 'Players', value: `${status.players}`, inline: true },
)
.setFooter({ text: 'Updated' })
.setTimestamp();
}
async function updatePanel() {
const channel = await client.channels.fetch(process.env.STATUS_CHANNEL_ID);
const status = await fetchStatus();
const embed = buildEmbed(status);
if (statusMessageId) {
try {
const msg = await channel.messages.fetch(statusMessageId);
await msg.edit({ embeds: [embed] });
return;
} catch { statusMessageId = null; }
}
const sent = await channel.send({ embeds: [embed] });
statusMessageId = sent.id;
}
client.once('ready', () => {
console.log(`Bot online as ${client.user.tag}`);
updatePanel();
setInterval(updatePanel, 60_000);
});
client.login(process.env.DISCORD_TOKEN);
Step 6 — Slash commands
Beyond the automatic panel, give the player an on-demand /status command. Register the command with a deploy script and handle the interaction in the bot:
// deploy-commands.js
require('dotenv').config();
const { REST, Routes, SlashCommandBuilder } = require('discord.js');
const commands = [
new SlashCommandBuilder().setName('status')
.setDescription('Shows the current server status').toJSON(),
];
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands }
).then(() => console.log('Commands registered')).catch(console.error);
In index.js, add the interaction listener, reusing fetchStatus() and buildEmbed() to respond with the same embed. Guild commands appear within seconds; global commands can take up to an hour to propagate.
Step 7 — Keep the bot alive with PM2
The bot process needs to restart itself if it crashes. Use PM2:
npm install -g pm2
pm2 start index.js --name statusmu
pm2 save
pm2 startup
With pm2 logs statusmu you follow the output in real time, and pm2 restart statusmu reloads after changes.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Bot shows "online" with the server down | Only checks the database, not the GameServer | Add a TCP port test against the GameServer |
ECONNREFUSED 1433 | TCP/IP disabled or port blocked | Enable TCP/IP in SQL Config Manager and open it in the firewall |
| Slash commands do not appear | Deploy not run or wrong scope | Run deploy-commands.js and check CLIENT_ID/GUILD_ID |
| Panel creates a new message every cycle | Message ID does not persist | Save the statusMessageId to a file/DB so it survives a restart |
Invalid Token | Wrong or regenerated token | Copy the current token in Bot → Reset Token and update the .env |
| Rate limit when editing | Interval too short | Increase the interval to 60s or more |
Security best practices
Never commit the .env. Create a read-only database user, with GRANT SELECT only on the necessary tables, and never use the sa account. If you expose a status API on the site, return only the strictly public fields — never passwords, serials, or emails. Prefer having the bot consume the API rather than opening the database port to the internet. And document the refresh interval so future maintenance doesn't lower it to the point of triggering rate limits.
Launch checklist
- Node.js 18+ installed on the bot's machine
- Application and bot created in the Developer Portal, token stored in
.env - Bot added to the Discord server with permission to send messages and embeds
- Data source defined (site API or read-only database)
- GameServer TCP port test implemented
- Status channel created and its ID configured
- Automatic panel updating every 60s
/statuscommand registered and responding- PM2 configured with
pm2 saveandpm2 startup .envoutside version control and with restricted permissions
Frequently asked questions
Does the bot need to run on the same VPS as the MU server?
No. It only needs access to the database or to an HTTP endpoint on the website. It can run on another machine, as long as the SQL Server port (1433, for example) is open to it or the site exposes a status API.
What is the difference between reading the database directly and using a site API?
Reading the database is more direct but exposes SQL on the network. An HTTP API on the site itself is safer because you control exactly which data goes out and you never open the database port to the outside world.
Why does my bot show 'online' even when the server is down?
You are probably only checking the database connection, and the database stays up even when the GameServer is stopped. Also run a TCP port test against the GameServer so the bot reflects the real state.
How often should the bot refresh the status?
A 60-second interval is safe. Refreshing more often than every 15 seconds can run into Discord rate limits when editing the same message several times per minute.
Is it safe to leave the bot token in the code?
Never. The token grants full control over the bot. Keep it in a .env file outside version control and restrict the file's permissions on the server.