commit 87a9013ffb31b2e13a6dbabe9d22a04594b17d8a Author: Nova Date: Wed Feb 11 21:05:29 2026 +0000 Initial commit: Minecraft server monitoring skill - Basic server status checking (online/offline) - Player counts, latency, version info - Example: corejourney.org - No external dependencies required diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..52389ef --- /dev/null +++ b/SKILL.md @@ -0,0 +1,85 @@ +--- +name: minecraft-monitor +description: Monitor Minecraft servers by checking online status, player counts, latency, and version info using the Server List Ping protocol. Use when the user asks to check Minecraft server status, monitor a Minecraft server, verify if a server is online, get player counts, or mentions Minecraft server monitoring. Example servers include corejourney.org. +--- + +# Minecraft Server Monitoring + +Quickly check Minecraft server status without installing any external dependencies. + +## Quick Check + +Check if a server is online: + +```bash +python3 ~/.openclaw/workspace/skills/public/minecraft-monitor/scripts/minecraft-status.py corejourney.org +``` + +``` +🟢 corejourney.org:25565 - ONLINE (45ms) + Version: 1.20.4 + Players: 3/20 + Online: Steve, Alex, CreeperHunter + MOTD: Welcome to Core Journey! +``` + +## Usage + +```bash +python3 ~/.openclaw/workspace/skills/public/minecraft-monitor/scripts/minecraft-status.py [timeout] +``` + +- **host**: Server hostname or IP address (e.g., `corejourney.org`, `192.168.1.10`) +- **port**: Optional, defaults to `25565` +- **timeout**: Optional connection timeout in seconds (default: 5) + +### Examples + +```bash +# Check default port +python3 ~/.openclaw/workspace/skills/public/minecraft-monitor/scripts/minecraft-status.py corejourney.org + +# Check custom port +python3 ~/.openclaw/workspace/skills/public/minecraft-monitor/scripts/minecraft-status.py corejourney.org:25566 + +# Check IP with longer timeout +python3 ~/.openclaw/workspace/skills/public/minecraft-monitor/scripts/minecraft-status.py 192.168.1.10 10 +``` + +## Output + +**Online server:** +- 🟢 Green (good ping) / 🟡 Yellow (moderate) / 🟠 Orange (slow) +- Server address and port +- Response time in milliseconds +- Minecraft version +- Current/maximum player count +- List of online players (up to 5 shown) +- Server MOTD (message of the day) + +**Offline server:** +- 🔴 Red indicator +- Error message (timeout, connection refused, etc.) + +## What's Being Monitored + +- ✅ Online/offline status +- ✅ Player count (current/max) +- ✅ Response time/latency +- ✅ Server version +- ✅ Online player list (if available) +- ✅ Server MOTD + +## Notes + +- Uses Minecraft Server List Ping (SLP) protocol - works with all modern Minecraft servers +- No server-side plugins or RCON access required +- Exit code 0 if online, 1 if offline (useful for scripts/automation) +- SRV records are not automatically resolved - use the actual server address + +## Integration Ideas + +- Add to a cron job for periodic health checks +- Wrap in a monitoring script that alerts if server goes offline +- Use in automation pipelines that depend on server availability +- Create a dashboard showing server status history \ No newline at end of file diff --git a/scripts/minecraft-status.py b/scripts/minecraft-status.py new file mode 100755 index 0000000..b571d8d --- /dev/null +++ b/scripts/minecraft-status.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +""" +Minecraft Server Status Checker + +Queries a Minecraft server for basic status information: +- Online status +- Player count (current/max) +- Server motd/message +- Ping response time +- Version information + +Uses Minecraft Server List Ping protocol (SLP). +""" + +import socket +import struct +import json +import sys +import time + +def encode_varint(num): + """Encode an integer as a Minecraft varint.""" + if num < 0: + num = (1 << 32) + num + out = b"" + while num > 0x7F: + out += bytes([(num & 0x7F) | 0x80]) + num >>= 7 + out += bytes([num]) + return out + +def decode_varint(sock): + """Decode a Minecraft varint from socket.""" + num = 0 + shift = 0 + while True: + byte = sock.recv(1) + if not byte: + raise IOError("Connection closed") + byte = byte[0] + num |= (byte & 0x7F) << shift + if not (byte & 0x80): + break + shift += 7 + return num + +def send_handshake(sock, host, port): + """Send handshake packet.""" + data = b"" + # Packet ID (handshake = 0x00) + data += b"\x00" + # Protocol version (use latest: 765 for 1.19.3+) + data += encode_varint(765) + # Server address + host_bytes = host.encode('utf-8') if isinstance(host, str) else host + data += encode_varint(len(host_bytes)) + data += host_bytes + # Server port + data += struct.pack(">H", port) + # Next state (status = 1) + data += b"\x01" + + # Length prefix + length = encode_varint(len(data)) + sock.send(length + data) + +def send_status_request(sock): + """Send status request packet.""" + data = b"\x01\x00" # Request (empty) + length = encode_varint(len(data)) + sock.send(length + data) + +def read_status_response(sock): + """Read server status response.""" + # Packet length + length = decode_varint(sock) + # Packet ID should be 0x00 + packet_id = decode_varint(sock) + + # Response length + response_len = decode_varint(sock) + # Response data (JSON) + response = b"" + while len(response) < response_len: + response += sock.recv(response_len - len(response)) + + return json.loads(response.decode('utf-8')) + +def minecraft_status(host, port=25565, timeout=5): + """Get Minecraft server status.""" + try: + start_time = time.time() + + # Connect to server + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + sock.connect((host, port)) + + # Handshake + send_handshake(sock, host, port) + send_status_request(sock) + + # Read response + data = read_status_response(sock) + + sock.close() + + ping_ms = round((time.time() - start_time) * 1000, 1) + + # Parse response + result = { + 'online': True, + 'host': host, + 'port': port, + 'ping': ping_ms, + } + + # Version info + if 'version' in data: + result['version'] = data['version'].get('name', 'Unknown') + result['protocol'] = data['version'].get('protocol', 'Unknown') + + # Player count + if 'players' in data: + players = data['players'] + result['players_online'] = players.get('online', 0) + result['players_max'] = players.get('max', 0) + if 'sample' in players and players['sample']: + result['player_list'] = [p['name'] for p in players['sample']] + + # MOTD (message of the day) + if 'description' in data: + motd = data['description'] + if isinstance(motd, dict): + motd = motd.get('text', '') + result['motd'] = motd + + return result + + except socket.timeout: + return { + 'online': False, + 'host': host, + 'port': port, + 'error': 'Connection timed out' + } + except ConnectionRefusedError: + return { + 'online': False, + 'host': host, + 'port': port, + 'error': 'Connection refused' + } + except Exception as e: + return { + 'online': False, + 'host': host, + 'port': port, + 'error': str(e) + } + +def format_status(status): + """Format status for human-readable output.""" + if not status['online']: + print(f"🔴 {status['host']}:{status['port']} - OFFLINE") + print(f" Error: {status.get('error', 'Unknown')}") + return + + emoji = "🟢" if status['ping'] < 100 else "🟡" if status['ping'] < 200 else "🟠" + print(f"{emoji} {status['host']}:{status['port']} - ONLINE ({status['ping']}ms)") + print(f" Version: {status.get('version', 'Unknown')}") + print(f" Players: {status.get('players_online', 0)}/{status.get('players_max', 0)}") + + if 'player_list' in status and status['player_list']: + print(f" Online: {', '.join(status['player_list'][:5])}" + + (f" ... +{len(status['player_list'])-5} more" if len(status['player_list']) > 5 else "")) + + if 'motd' in status and status['motd']: + motd = status['motd'].strip() + if motd: + print(f" MOTD: {motd[:80]}{'...' if len(motd) > 80 else ''}") + +def main(): + if len(sys.argv) < 2: + print("Usage: python minecraft-status.py [timeout]") + print("") + print("Examples:") + print(" python minecraft-status.py corejourney.org") + print(" python minecraft-status.py corejourney.org:25565") + print(" python minecraft-status.py 192.168.1.10:25566 10") + sys.exit(1) + + # Parse host:port + hostport = sys.argv[1] + if ':' in hostport: + host, port_str = hostport.rsplit(':', 1) + try: + port = int(port_str) + except ValueError: + print(f"Invalid port: {port_str}") + sys.exit(1) + else: + host = hostport + port = 25565 + + # Parse timeout + timeout = int(sys.argv[2]) if len(sys.argv) > 2 else 5 + + status = minecraft_status(host, port, timeout) + format_status(status) + + # Return exit code 0 if online, 1 if offline + sys.exit(0 if status['online'] else 1) + +if __name__ == '__main__': + main() \ No newline at end of file