Initial commit: Minecraft server monitoring skill
- Basic server status checking (online/offline) - Player counts, latency, version info - Example: corejourney.org - No external dependencies required
This commit is contained in:
85
SKILL.md
Normal file
85
SKILL.md
Normal file
@@ -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 <host[:port]> [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
|
||||
216
scripts/minecraft-status.py
Executable file
216
scripts/minecraft-status.py
Executable file
@@ -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 <host[:port]> [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()
|
||||
Reference in New Issue
Block a user