MCConnect is a further development of my other mc-stats site.
This will be version 2.0, with an improved and optimized backend with Postgres. Unlike before, this program no longer needs to run on the same file system as the Minecraft server. In addition, the website will support multiple Minecraft servers.
Anyone will be able to add their own Minecraft server to this server with a simple, lightweight minecraft plugin.
Preview:
-
Player list and status information for every player ever joined the server

-
Privacy friendly and easy login for every player to view and change personal data:

-
Rankings, player comparison, teams (prefixes), competitions, achievements, server statistics, weekly recap and an activity feed
-
And a lot more...
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
docker compose -f docker/db/docker-compose.yml -p mcconnect up -d # local postgres
.venv/bin/python -m pytest -q -p no:logging # ~470 tests, uses database mcconnect_test
.venv/bin/python -m database.manage seed # dev admin 'tobi' + server 'testdomain'
MCC_BASE_DOMAIN=mc.t-auer.local:5050 .venv/bin/python mc_socket/main.py # plugin socket on :9991 (TLS, own certificate)
FLASK_SERVER_NAME=mc.t-auer.local:5050 .venv/bin/python -c "from web.main import create_app; create_app().run(port=5050)"Port 5000 is taken by the macOS AirPlay receiver, hence 5050. python -m mc_socket.tlscert prints the
fingerprint of the local socket certificate (certs/) for a test plugin's tls-fingerprint.
database/– DB layer (databaseManagerV2.py), schema + migrations, stats classification, metrics, achievements, motivation, rewards, config (MCC_*env vars)mc_socket/– socket server the plugin connects to (protocol documented inmain.py), in-game commands (commands.py), TLS certificate (tlscert.py)web/– Flask app (main domain: admin area;<subdomain>.: server pages)java plugin/MCDataLink/– Spigot/Paper plugin (mvn package)tools/e2e/– end-to-end test with a real Paper server and botsdeploy/– production docker compose (behind Nginx Proxy Manager, optional traefik); see deploy/README.md
State of 2026-10-22: schema version 30, plugin 3.24 (block registry). Written as a handoff for the next development session.
| Area | Where | Notes |
|---|---|---|
| Rankings | /rangliste |
19 metrics in 4 groups (database/metrics.py), all time / 30 / 7 days; + buttons collect players for the comparison (web/static/compare.js) |
| Comparison | /vergleich |
2–4 players, best values, history chart, detail tables |
| Player list | /spieler |
search, sort, "last online" filters, badges, top places, favourites |
| Player page | /spieler?player= |
cards (same values as rankings, player_card_values), activity chart, ranking places, highlights, achievements (live), trophy cabinet, bio, stat card (/spieler/<name>/karte.png, og:image), guestbook, moderator notes and warnings. Tobi's own design: keep the desktop look, phone rules only in the 600px block of responsive-player-info.css |
| Server statistics | /server-statistik |
online history, peak-time heatmap, totals, new players, fun facts; based on player_sessions |
| Start page | / |
records, weekly recap, running competitions and goals, events, polls, activity feed (/api/feed) |
| Achievements | database/achievements.py |
13 × 4 tiers (player_achievements); tiers reached while not playing are silent (no feed, no chat) |
| Teams | /teams |
prefixes as teams |
| Motivation | database/motivation.py, /wettbewerbe, /ruhmeshalle |
competitions, community goals, streaks + badges 7/30/100, anniversaries (player_milestones), record history (record_history), trophies (competition places, player of the week), hall of fame; news in feed and chat |
| In game | mc_socket/commands.py |
/stats, /top, /wettbewerb, /duell, /report, /seitenleiste, /vote, /events, /joinmessage, moderators /verwarnen, /stumm, /entstummen (!CMD → !tell, clickable buttons); scoreboard sidebar |
| Community | /events, /umfragen, /galerie, /duelle, player page |
events with sign up and chat reminder, polls, build gallery with approval and likes, duels, guestbook |
| Access | /mitmachen, /regeln |
whitelist by application and/or invite code (the plugin runs whitelist add; the kick message links to /mitmachen), optional rules & FAQ page |
| Addons | database/addons.py, /manage/<id>#addons (owners), java plugin/addons/ |
plug-and-play extensions through MCDataLink (no own key): store with one-click install (download + SHA-256 check + live load), approval by the owner (bound to the jar's checksum, official releases automatically), settings page built from the addon's mcconnect-addon.json, in-game changes checked and stored by MCConnect, settings cached by the plugin for offline starts; the owner can let moderators edit the settings (/users/erweiterungen) and switch on automatic updates of official addons. Official: BetterJoinMessages, EasyHeadDrop, EasyNewCraftingRecipes, SpawnElytra, SpawnProtect. Decision and security limits: docs/adr/0001-addon-system.md, writing an addon: java plugin/addons/README.md |
| Join messages & rewards | database/rewards.py, /profil, /users/belohnungen, /joinmessage |
only with the addon BetterJoinMessages (since 3.22; without it everything here is off and the game's message is shown); reward levels (default 6, editable per server; conditions are alternatives ("oder") of groups that must all be met ("und"), unlocks add up, a reached level is stored and never lowered); players choose text, leave text, color, symbol, style and sound from what they unlocked (templates only, moderators add own texts; moderators/OPs also gold/red/dark red); while on, everyone gets an MCConnect message (without a choice the first level's default, !joindefault); players can hide others' messages and all sounds |
| Moderation | /users (moderators) |
overview (open tasks, server health, newest log entries) and sub pages spieler (reports, bans, warnings, X-ray hints, activity), inhalte (gallery, guestbook, events, polls, competitions, goals), zugang (whitelist, codes, rules/FAQ), belohnungen, protokoll; templates in web/templates/mod/ |
| Admin | /manage (server owners) |
server tiles; /manage/<id> with tabs: overview (health, e-mail alerts), plugin (config with tls-fingerprint), server page (texts, images, directory), moderation (moderators, bans), danger zone |
| Year in review | /rueckblick/<name> |
year gains from the first/last snapshot of the year (both kept forever), sessions, achievements, trophies |
| Reach & design | main page, web/static/css/dark.css |
public server directory (servers.listed), dark mode for all server pages (_theme.html, --srv-* tokens) |
| Icons & name line | database/glyphs.py, database/badges.py, web/static/glyphs/ |
13 drawn 9×9 pixel icons (SVG → PNG without extra libraries) in a resource pack (/resourcepack/<sha1>.zip, private use area U+E000…); 8 of them are join symbols unlocked by the default levels (servers with own levels tick them in the editor); under the name up to 4 values (default trophies, streak, level; player_server_info.name_badges), nothing for hide_stats. The plugin offers the pack and shows Unicode fallbacks to players without it (per viewer; icon codepoints are stored – only append) |
| Own icons | /users/icons, server_icons, glyphs.IconSet |
moderators draw up to 32 own 9×9 icons (pixel editor, an image is shrunk to 9×9 in the browser) with a fallback character; usable as join symbols (tick them at the levels) and by everyone in prefixes as :name:; codepoints from U+E100, never given out twice (servers.icons_next); a deleted icon becomes its fallback in prefixes; the pack is per server (/resourcepack/<server id>/<sha1>.zip) |
| Profile & privacy | /profil |
bio, "hide my stats" (hide_stats: left out of every public view except server totals), favourites, sidebar, join message |
Charts are plain SVG without libraries (web/static/*-chart.js), styles in web/static/css/stats.css.
3.1 !broadcast · 3.2 !HEALTH · 3.3 live stats · 3.4 in-game commands, sidebar · 3.5 /vote, /events ·
3.6 mutes, whitelist sync, join link, moderator commands · 3.7 !WEBBANS (/pardon lifts a website ban) ·
3.8 optional TLS · 3.9 times in the local time zone · 3.10 clickable chat (⟦label⇒/command⟧, !FEATURES~click) ·
3.11 buttons only run the plugin's own commands · 3.12 always TLS on 9991 (tls-fingerprint, TlsPinning) ·
3.13 live stats every 5 s (live-stats-interval; events mark what changed) · 3.14 join/leave messages
(JoinMessages, !joinstyle, !joinmutes, !joinreset), /joinmessage · 3.15 !JOIN sends the game's first
join, the first live update after a join is complete · 3.16 default join lines for everyone (!joindefault) ·
3.17 icons (!FEATURES~…,glyphs, !pack, fallbacks per viewer, hint with /icons after every decline) and the line under the names (!badge, Paper 1.20.3+ by
reflection; players with the pack get a copy of the main scoreboard with MCConnect's teams). · 3.18 automatic updates
(!update, database/plugin_jar.py: version and SHA-256 of the offered jar over the pinned connection, a newer jar
goes into plugins/update/ under the running jar's name and is active after the next restart; auto-update: false) ·
3.19 own icons in prefixes: above the heads and in the chat per viewer (players with the pack get the chat line with icons
as a message of their own), the tab list always with fallbacks (it is the same for everyone) · 3.20 tooltips in chat
lines (database/chat.py: ⟦label⇝line¶line⟧; achievements, records, streaks, goals, competitions, player of the
week, duels and polls show 2–4 lines of details on hover; older plugins get the label only). · 3.21 the live
play time is named play_time on the year versions (26.x) too; 3.3–3.20 sent play_one_minute there, which was
stored next to the play_time of the stats file and counted twice (fixed on the server by stats.ALIASES and
migration 27 for every plugin version). · 3.22 addons (AddonManager, AddonInstaller, API in
org.tobias.mcdatalink.api; !FEATURES~…,addons, !ADDONS, !addonstate, !addon/!ADDON, !ADDONCONFIG,
!addoninstall, !addonremove, !ADDONRESULT, !ADDONPROBLEMS; addon-install: false switches remote installs off);
join messages moved into BetterJoinMessages, /joinmessage is its command. · 3.23 !whitelist~remove (!FEATURES~…,unwhitelist): moderators take
absent players off the whitelist on /users/spieler (no console commands any more). · 3.24 !REGISTRY after auth:
the server's blocks and items (RegistryReporter: Material plus Paper's Registry.BLOCK/ITEM by reflection, so
also mods on hybrid servers and data packs), stored per server (server_registry).
Plugins before 3.12 are refused (TLS only). Older plugins keep working but miss the newer features.
- Gains and baselines. Gains (today, 7/30 days, competitions, goals, duels, sidebar, weekly recap) are the
difference between daily snapshots (
stat_snapshots, written with every stats update). A player's first sync is also stored as the day before (baseline). New or old player decides that baseline: a player whose first join in the game (player_server_info.first_played, from plugin 3.15) lies afterservers.tracking_since(first plugin connection) is new – baseline 0, everything counts; otherwise the first stats are the baseline (an old player's lifetime values are no gain). When the first join arrives later,_correct_first_baselinefixes the guess once, in both directions. Without it (plugins before 3.15) the time since MCConnect's first sight is the fallback. This went wrong several times (schema 19, 21, 23, 24: players who joined while the plugin was disconnected, partial first live updates) – keep the tests intests/test_metrics.pygreen. - One source for every number. Rankings, comparison, player list, player page cards, records, stat card and
/statsall use the metrics ofdatabase/metrics.py(get_server_metrics/get_player_metrics). Do not read singleactionsrows for a value that is also a metric. Stat names are stored canonical (stats.canonical_name: namespaced, old names viastats.ALIASES), so a value can never be stored twice. Records show the current best value (record_holders),record_historyonly knows who took a record when. Metrics that add several objects say so (Metric.note, e.g. ores "inkl. Deepslate"); the single values on the player page and in the comparison details are per object and all time.tests/test_mobile.pychecks this across the pages. - Mobile. Never change the desktop design for phones: phone rules go into media queries only. The header becomes a bar with a menu panel below 576px (the original breakpoint) (
responsiveHeader.css);css/mobile.css(loaded byheader.htmlafter the page styles) holds the shared phone rules: tap targets of 40–44px, 16px inputs, wrapping segmented controls, tables with classmcc-stackbecome one card per row (every<td>needs adata-label).tests/test_mobile.pyrenders the pages in Chrome at 375px (Playwright fromrequirements-dev.txt, uses the installed Chrome; skipped without) and fails if anything runs out of the screen. - "Dabei seit", veterans, anniversaries, new players per week/month and the reward condition "days" use
the game's first join if known (
COALESCE(first_played, first_seen)). - Blocks of new versions and mods. Whether a stat object is a block decides its category ("blocks placed" counts
BLOCK_USEDonly). The lists grow from three sources (database/registry.py,docs/adr/0002-block-registry.md): the vanilla registry shipped indatabase/registry.json, the one of misode/mcmeta fetched by the socket server every day (every release and snapshot;MCC_REGISTRY_URL=offswitches it off) and the plugin's report of the running server (3.24, per server). Names are only added. Values stored as an item before a name was known as a block move to the block categories, and the player's snapshots move by the same amount, so the correction is no gain. Updateregistry.jsonwith a new Minecraft release (see the ADR). - Live values. The plugin sends custom stats and changed block/item/mob stats every 5 s, all blocks once
a minute. The socket server checks achievements, records and streaks at most every 15 s per player and
updates sidebars 5 s after new stats. Pages poll (
web/static/poll.js,LiveCache, 2 s per server/player);/wettbewerbeand/duellerefresh their standings every 10 s. - No page reloads. Forms and buttons are saved with
fetch; afterwardsrefreshLiveParts()(poll.js) replaces only the parts markeddata-live="…". Moderation pages useweb/static/mod.js(data-mod-post,data-mod-form,data-settings-form,data-setting). - Plugin connection. TLS only on 9991. The socket server creates its own certificate on the first start
(
mc_socket/tlscert.py, docker volumesocket_tls); the admin page shows its fingerprint in the plugin config, the plugin pins it. An own certificate (MCC_SOCKET_TLS_CERT/KEY) is optional. - Streaks count days with a session and days on which the play time grew (online while the plugin was disconnected, no session).
- The once-a-minute checks of the socket server run step by step, each guarded ("Periodic check failed: " in the log).
- Free text in chat messages always goes through
commands.clean()(no&codes, no button or tooltip markers); the plugin only lets buttons run its own commands. - A failing
!CMDanswers the player with an error, a failing request getserror|005; neither ends the plugin connection. Before!AUTHmessages are limited to 1 KB. - Rate limits (
RateLimiterinweb/main.py, in memory per worker): admin login per address and account, login pins per player and address, invite codes and applications per address. Behind a proxyFLASK_PROXY_FIXmust be on and port 8000 must not be reachable directly (the production setup is fine: private network). - Stats from the plugin are only stored for valid resource locations (
stats.OBJECT_NAME_RE) and bigint values. - Every response has
X-Frame-Options,nosniffand a referrer policy; HTML pages a CSP (content_security_policy()): scripts from the own origins, cdnjs, googleapis and jsdelivr (jQuery, three.js, MineRender on the player page) and inline<script nonce="{{ csp_nonce() }}">only. New inline scripts need the nonce;onclick=does not work – usedata-on-click="function"(web/static/actions.js) oraddEventListener. MineRender's page view counter (minerender.org) is blocked on purpose. - The dialog library (
messageBoxes.js) is vendored inweb/static/vendor/messageBoxLib/(fixed commit).
- The owner's visual feedback on the latest rounds is still missing.
- Invite codes can only be redeemed on the website (a player who is not on the whitelist cannot join the game).
- The year in review only knows gains since the snapshots started (schema 7).
- Players who never joined while the plugin was connected and have no first join from the game (plugins before 3.15) are guessed as old players.
- UI texts in German with "du", code/comments/commits in English, no co-author or "generated" lines in commits. Minecraft names the way players say them (Ancient Debris, not "Antiker Schutt"). A calm, serious look: fonts Chakra Petch / Atkinson Hyperlegible / JetBrains Mono (self-hosted), no playful animations, no external CDNs for new things.
- Take screenshots before finishing (headless Chrome with
--host-resolver-rules; pages that need a login are rendered withapp.test_client()andsession_transaction). - New migration: add
N: [...]toMIGRATIONSindatabase/databaseManagerV2.pyand drop the new objects intest_migration_from_version_1(tests/test_database.py). - Every new public view must respect
hide_stats(get_server_metrics()leaves hidden players out unlessinclude_hidden=True). - No CSS class or id may start with
ad-,ads-or contain-ad-/banner-ad: ad blockers hide such elements (the addon store was invisible behind uBlock with its firstad-prefix; it usesext-now). - New CSS uses the
--srv-*/--st-*tokens (or gets a rule indark.css), otherwise it stays light in dark mode. - Plugin changes: bump the version in
pom.xml, document new messages in the docstring ofmc_socket/main.py, check that it compiles (docker run --rm -v <copy>:/p -w /p maven:3.9-eclipse-temurin-17 mvn -q package) and test it withtools/e2e/(bots in a Paper server;ui-test.jsclicks through the website). Production builds the plugin in the Dockerfile; server owners download it on the admin page. - Plugins and addons:
mvn packageinjava plugin/(aggregator; addon jars are reproducible, MCConnect approves official ones by their SHA-256). Paper loads plugins from remapped copies – the addon's original jar is looked up inplugins/(AddonManager.jarOf). - Dev data: the dev database has fake players, sessions, snapshots and health samples on
testdomain; more can be created withensure_player_on_server,update_player_statsand by movingstat_snapshots.day/player_sessionsinto the past.
Legal information:
-
The source code of this software is available for viewing, but no part of this software may be copied, modified, distributed, or used for any commercial purposes without explicit permission from the author.
-
The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
-
Any use of the software not expressly permitted by this license is strictly prohibited.

