Lightweight Minecraft API + infrastructure toolkit: player profiles & textures, Java/Bedrock server status probes, and Votifier (v1/v2) clients for Node and other fetch-capable runtimes. Some helpers rely on Node networking or binary modules.
This toolkit wraps Mojang APIs. Rate limits and availability still apply. Write endpoints (name change, skin upload) are not yet included.
ESM-only: this package ships as
"type": "module"with no CommonJS build. Useimport, or dynamicimport()from CommonJS.
# ✨ Auto-detect
npx nypm install minecraft-toolkit
# npm
npm install minecraft-toolkit
# yarn
yarn add minecraft-toolkit
# pnpm
pnpm install minecraft-toolkit
# bun
bun install minecraft-toolkit
# deno
deno install minecraft-toolkitimport {
fetchPlayerProfile,
fetchPlayerSkin,
fetchPlayerUUID,
fetchPlayerSummary,
fetchPlayers,
resolvePlayer,
fetchSkinMetadata,
} from "minecraft-toolkit";
const profile = await fetchPlayerProfile("26bz");
const summary = await fetchPlayerSummary("26bz");
const skin = await fetchPlayerSkin("26bz");
const uuid = await fetchPlayerUUID("26bz");
const batch = await fetchPlayers(["Notch", "26bz"], { delayMs: 50 });
const resolved = await resolvePlayer("069a79f444e94726a5befca90e38aaf5");
const skinMeta = await fetchSkinMetadata("26bz");Fetch-based helpers run anywhere fetch exists (Node 18+, Bun, Workers). Node networking and PNG helpers are not edge-safe. All API-style failures surface as MinecraftToolkitError.
import {
isValidUsername,
isUUID,
normalizeUUID,
uuidWithDashes,
uuidWithoutDashes,
getSkinURL,
getCapeURL,
getSkinModel,
extractTextureHash,
} from "minecraft-toolkit";
isValidUsername("26bz"); // true
uuidWithDashes("069a79f444e94726a5befca90e38aaf5");
const profile = await fetchPlayerProfile("26bz");
const skinUrl = getSkinURL(profile);
const hash = extractTextureHash(skinUrl);
const model = getSkinModel(profile); // "slim" | "default"import { fetchSkinMetadata, computeSkinDominantColor } from "minecraft-toolkit";
const meta = await fetchSkinMetadata("26bz", {
dominantColor: true,
sampleRegion: { x: 8, y: 8, width: 8, height: 8 },
});
console.log(meta.dominantColor); // e.g. "#f2d2a9"
const accent = await computeSkinDominantColor(meta.skin.url, {
x: 40,
y: 8,
width: 8,
height: 8,
});Composite a skin texture into a ready-to-use PNG (buffer, base64, and dataUri), without pulling in a canvas dependency. Accepts a username, UUID, or a raw skin URL.
import { renderPlayerHead, renderPlayerBust } from "minecraft-toolkit";
const head = await renderPlayerHead("26bz", { size: 128 }); // face + hat overlay
const bust = await renderPlayerBust("26bz", { size: 128 }); // head + torso + arms
console.log(head.dataUri); // "data:image/png;base64,..."overlay(defaulttrue) toggles the hat/jacket/sleeve overlay layers.modelforces"default"or"slim"arm width when rendering from a raw skin URL (model is auto-detected when resolving by username/UUID).renderPlayerBustmirrors the right arm for legacy 64x32 skins that don't carry left-limb pixel data.sizeis an upscale target for the image's longest native dimension using nearest-neighbor scaling (integer multiples only, so output size is always a whole multiple of the native resolution). It never downscales below native resolution — requesting asizesmaller than native returns the unscaled image.
The internal response cache and a rate-limit-aware retry helper are exposed for building your own resilient wrappers around any of the fetch-based helpers.
import { createCache, withCache, withRetry, fetchPlayerProfile } from "minecraft-toolkit";
const cache = createCache({ ttlSeconds: 30 });
const profile = await withRetry(() =>
withCache(cache, "profile:26bz", () => fetchPlayerProfile("26bz")),
);withRetry(fn, options)retries only onMinecraftToolkitErrorwithstatusCode: 429, honoring the MojangRetry-Afterheader (retryAfter) when present and falling back to exponential backoff (minDelayMs/maxDelayMs, default 3 retries).createCache({ ttlSeconds, maxSize })returns aResponseCache(ornullwhen{ cache: false });withCache(cache, key, resolver)is a no-op passthrough whencacheisnull.
Poll a server on an interval and only get notified when something actually changes (online state, player count, or MOTD) — no need to hand-roll diffing for a status page or Discord bot.
import { watchServerStatus } from "minecraft-toolkit";
const watcher = watchServerStatus("mc.hypixel.net", {
intervalMs: 30_000,
onChange: (status, previous) => console.log(`players: ${status.players.online}`),
onError: (error) => console.error(error),
});
// later
watcher.stop();onUpdate fires on every poll; onChange fires only when online, players.online, players.max, or motd differ from the previous poll. Accepts the same options as fetchServerStatus. intervalMs is clamped to a 1000ms floor to avoid hammering the target server.
A valid Microsoft/Xbox Live access token is required for minecraftservices.com endpoints. Missing or expired tokens throw MinecraftToolkitError with statusCode: 401.
import {
fetchNameChangeInfo,
checkNameAvailability,
validateGiftCode,
fetchBlockedServers,
} from "minecraft-toolkit";
const accessToken = process.env.MC_ACCESS_TOKEN;
const windowInfo = await fetchNameChangeInfo(accessToken);
const availability = await checkNameAvailability("fresh_name", accessToken);
const isGiftValid = await validateGiftCode("ABCD-1234", accessToken);
const blockedServer = await fetchBlockedServers(); // no token requiredvalidateGiftCode returns true/false for 200/404 responses without throwing.
Probe Java and Bedrock servers without bringing your own RakNet/TCP logic.
import {
fetchServerStatus,
fetchJavaServerStatus,
fetchBedrockServerStatus,
} from "minecraft-toolkit";
const javaStatus = await fetchJavaServerStatus("mc.hypixel.net", { port: 25565 });
const bedrockStatus = await fetchBedrockServerStatus("play.example.net", { port: 19132 });
// fetchServerStatus picks the right probe based on the `edition` field
const autoStatus = await fetchServerStatus("my.realm.net", { edition: "bedrock" });
console.log(javaStatus.players.online, bedrockStatus.motd);Both helpers normalize MOTD text, favicon/Base64 icons, latency, and version info. Errors surface as
MinecraftToolkitError with contextual status codes.
Probe Java and Bedrock in parallel when you don't know which edition a server runs.
import { discoverServer } from "minecraft-toolkit";
const status = await discoverServer("play.example.net", {
javaPort: 25565,
bedrockPort: 19132,
timeoutMs: 3000,
});
console.log(status.edition, status.motd);discoverServer runs fetchJavaServerStatus and fetchBedrockServerStatus concurrently (so the round-trip is capped by the slower of the two, not their sum), returning the Java result if it succeeded and otherwise the Bedrock result. It throws MinecraftToolkitError only when neither probe succeeds.
import { fetchServerIcon } from "minecraft-toolkit";
const icon = await fetchServerIcon("play.example.net");
console.log(icon.base64); // "iVBOR..."
console.log(icon.byteLength); // raw PNG size in bytesThe helper reuses the Java status ping to extract the favicon, returning:
dataUri: ready-to-renderdata:image/png;base64,...base64: raw Base64 payloadbuffer+byteLengthfor further processing (e.g., resizing, hashing)
If the server doesn’t expose an icon, it throws MinecraftToolkitError (404).
Send vote notifications to classic Votifier v1 (RSA public key) and NuVotifier v2 (token/HMAC) servers without re-implementing either protocol.
import { sendVotifierVote } from "minecraft-toolkit";
const result = await sendVotifierVote({
host: "votifier.myserver.net",
port: 8192, // defaults to 8192 if omitted
publicKey: process.env.VOTIFIER_PUBLIC_KEY, // v1 servers
serviceName: "MyTopList",
username: "26bz",
address: "198.51.100.42",
token: listingSiteConfig.token, // v2 servers (optional)
protocol: "auto", // let the handshake decide between v1/v2
});
console.log(result.acknowledged, result.version, result.protocol);- Provide either a legacy RSA public key (for protocol v1) or a NuVotifier token (protocol v2). Server listing sites typically store each server's token and pass it here;
protocol: "auto"will select the right flow based on the handshake. timestampaccepts aDateor millisecond value (default:Date.now()). All failures bubble asMinecraftToolkitError.
Send admin commands to a Java server with RCON enabled (enable-rcon=true in server.properties).
import { createRconClient, sendRconCommand } from "minecraft-toolkit";
// one-shot: connects, authenticates, runs a single command, then closes
const list = await sendRconCommand({
host: "mc.example.net",
port: 25575,
password: process.env.RCON_PASSWORD,
command: "list",
});
// persistent client for multiple commands
const client = await createRconClient({
host: "mc.example.net",
password: process.env.RCON_PASSWORD,
});
await client.execute("say Server restarting in 5 minutes");
await client.execute("save-all");
client.close();portdefaults to25575. Authentication failures (wrong password) throwMinecraftToolkitErrorwithstatusCode: 401; connection/timeout issues surface as504/502.client.execute(command)can be called repeatedly on the same connection;client.close()tears down the socket and rejects any in-flight commands.- RCON sends commands and credentials in plaintext — only use it over a trusted network (VPN, SSH tunnel, or localhost), never expose the RCON port directly to the internet.
Convert legacy § or & codes into safe HTML fragments or CSS class spans.
import { toHTML, generateCSS, stripCodes, hasCodes, convertPrefix } from "minecraft-toolkit";
const motd = "§aWelcome §lHeroes§r!";
const inline = toHTML(motd); // <span style="color: #55ff55">Welcome ...</span>
const classes = toHTML(motd, { mode: "class", classPrefix: "mc" });
const css = generateCSS(); // drop into a <style> tag
stripCodes(motd); // "Welcome Heroes!"
hasCodes(motd); // true
convertPrefix("&aHi", "toSection"); // "§aHi"getMaps() exposes the color and format metadata if you want to build custom renderers.
Convert between legacy §/& codes, Minecraft's modern JSON text component format, and MiniMessage tags.
import {
legacyToComponent,
componentToLegacy,
componentToMiniMessage,
miniMessageToComponent,
legacyToMiniMessage,
miniMessageToLegacy,
} from "minecraft-toolkit";
legacyToComponent("§aHello §lWorld");
// [{ text: "Hello ", color: "green" }, { text: "World", color: "green", bold: true }]
componentToLegacy([{ text: "Hi", color: "red", bold: true }]); // "§c§lHi"
componentToMiniMessage([{ text: "Hi", color: "red", bold: true }]); // "<red><bold>Hi</bold></red>"
miniMessageToComponent("<red><bold>Hi</bold></red>");
// [{ text: "Hi", color: "red", bold: true }]
legacyToMiniMessage("§cHi"); // "<red>Hi</red>"
miniMessageToLegacy("<red>Hi</red>"); // "§cHi"legacyToComponent/miniMessageToComponentalways return an array of{ text, color?, bold?, italic?, underlined?, strikethrough?, obfuscated? }segments.componentToLegacy/componentToMiniMessageaccept a single component, an array of components, or a component with nestedextra(Minecraft's standard nesting shape).- MiniMessage parsing supports color names,
<#rrggbb>hex,<color:name>/<colour:name>, format tags (bold/b,italic/i,underlined/u,strikethrough/st,obfuscated/obf), and<reset>. It's a practical subset — click/hover events and gradients aren't supported. - Colors without a matching legacy color code (arbitrary hex) are dropped when converting to legacy output, since legacy codes can't represent them.
Mount ready-made REST endpoints for the player, account, and server helpers on top of h3. This is a separate subpath export so that consumers who only need the plain function calls above aren't forced to install h3 — add it yourself first:
pnpm add h3import { createPlayerApp } from "minecraft-toolkit/h3";
const { app } = createPlayerApp();
export default app; // or app.request(...) directly, or mount into an existing H3 appcreatePlayerApp(options?)returns{ app, handlers }, a standaloneH3app with every route below wired up.createPlayerHandlers()returns the rawdefineHandlerfunctions if you want to mount them yourself under different paths.playerPluginis anH3plugin (app.register(playerPlugin)/ pass to an existing app) that adds the same routes without creating a new app instance.
| Method | Path | Notes |
|---|---|---|
| GET | /player/:username |
fetchPlayerProfile |
| GET | /player/:username/skin |
fetchPlayerSkin |
| GET | /player/:username/summary |
fetchPlayerSummary |
| GET | /player/:username/uuid |
fetchPlayerUUID |
| GET | /player/:input/resolve |
resolvePlayer |
| GET | /player/:username/exists |
playerExists |
| POST | /players/batch |
fetchPlayers (body: { usernames, delayMs? }, max 100) |
| GET | /account/namechange |
requires Authorization: Bearer <token> |
| GET | /account/name/:name/availability |
requires Authorization: Bearer <token> |
| POST | /account/gift-code/validate |
requires Authorization: Bearer <token>, body { code } |
| GET | /account/blocked-servers |
fetchBlockedServers |
| GET | /server/:address/status |
fetchServerStatus; query: edition, port, timeoutMs, protocolVersion |
| GET | /server/:address/icon |
fetchServerIcon; query: port, timeoutMs, protocolVersion |
SSRF warning: the
/server/:address/statusand/server/:address/iconroutes accept an arbitraryaddress/portfrom the caller and open a raw socket to it, with no allowlist or private-IP blocking. If you mount this app on a publicly reachable server, anyone can use it to probe your internal network (loopback, RFC1918 ranges, cloud metadata endpoints) via response timing/content. Put these routes behind your own allowlist, auth, or network egress restrictions before exposing them publicly.