REST API reference for the Fabricator Minecraft server manager. Every endpoint lives under /api
and returns JSON, with two deliberate exceptions: snapshot download streams a binary archive
attachment, and world import takes raw archive bytes as its request body. Unknown /api/... paths
return 404 {"error": "endpoint not found", "path": ...} rather than the SPA's HTML.
Errors are consistently {"error": "<message>"}. Anything under /api not listed as public in
Authentication requires a session.
- Authentication
- Health
- Servers
- Server files
- Mods
- Console & metrics
- Loader versions
- Java management
- Players
- Backups & snapshots
- Modrinth integration
- playit.gg tunnel
- System updates
- Configuration
- Loader registry (developers)
- Modrinth client (Python)
- Worked examples
- Testing
- Security notes
Fabricator ships with a single-operator password login. The gate is default-deny: every /api/
route requires an authenticated session except the ones listed below.
States
- Disabled —
FABRICATOR_DISABLE_AUTH=1. The gate passes everything through. - Setup mode — auth is enabled but no credential exists yet. The app boots locked and only
serves
POST /api/auth/setup,GET /api/auth/statusandGET /api/healthuntil a password is set. - Configured — normal login. Public routes are
POST /api/auth/login,GET /api/auth/statusandGET /api/health; everything else needs the session cookie.
Unauthenticated requests get 401 {"error": "authentication required"} (or {"error": "setup required"}
in setup mode). The session cookie is HttpOnly, SameSite=Lax, and lives for 7 days.
First-boot only: sets the operator password and logs the caller in. JSON body required.
Body: { "password": "at-least-8-chars" }
Response (200): { "authenticated": true }
Errors: 400 invalid/short password or non-JSON body · 409 {"error": "already configured"}
Body: { "password": "..." }
Response (200): { "authenticated": true }
Errors: 400 password missing · 401 {"error": "invalid credentials"} (delayed ~1s)
Requires an active session and the current password.
Body: { "current": "...", "new": "at-least-8-chars" }
Response (200): { "changed": true }
Errors: 400 missing fields or new password too short · 401 current password incorrect ·
409 when the password is managed via FABRICATOR_AUTH_PASSWORD_HASH
Clears the session. Response (200): { "authenticated": false }
Response (200):
{ "enabled": true, "authenticated": false, "needs_setup": false }Response (200): { "healthy": true }
List all servers. Each record is augmented with a runtime block from the process registry
(live status, PID, RAM, mod count).
Create a server record. This only registers the server — call /install afterwards to lay down
the files.
Required fields: name, version, loader, port, installPath
Response (201): the created server, including a javaRequirement block describing the
required Java major, what was detected, and a recommended_install download hint.
Errors: 400 missing fields or the port is already used by another server · 500 on write failure
Single server, augmented with runtime state. 404 if unknown.
Update server settings and rewrite server.properties. id and createdAt are ignored if sent.
Two fields are launch tuning rather than server.properties, and take effect on the next start:
| Field | Meaning |
|---|---|
javaPath |
JVM to run this server on. A path is checked for existence and the executable bit; a bare command name (java, java21) is accepted and resolved on PATH at launch. Empty string clears the override, falling back to a managed runtime matching the MC version. |
jvmArgs |
Extra JVM flags, as the string the user typed (split shell-style at launch). Appended after the installer's own launch.jvm_args, so a repeated option resolves in the user's favour. Empty string clears them. |
jvmArgs refuses arguments that would contradict other settings or the installer: -Xmx/-Xms
and friends (use the memory field, or the two would silently disagree), -jar / -cp /
--class-path / --module-path, and @argfile. Max 2000 characters and 64 arguments.
Errors: 409 if the server is running (stop it first) · 404 unknown server · 400 invalid
javaPath / jvmArgs, with a message naming the offending value · 500 if server.properties
cannot be written
In managed mode javaPath, jvmArgs, command and launch are all rejected with 400 — the
deployment owns the JVM. POST /api/servers validates javaPath and jvmArgs the same way.
Set the boot auto-start mode. This is a Fabricator-level preference, so it can be changed while
the server is running and does not touch server.properties.
Body: { "mode": "always" | "never" | "last" }
Response (200): { "success": true, "autoStart": "always", "server": { ... } }
Stops the process, removes scheduled backups and backup records, deletes the server record, then deletes the install directory from disk.
Response (200): { "success": true, "message": "Server deleted successfully" }
Response (500): the record is gone but the files could not be removed — the body carries
error and success: false.
Starts the install asynchronously. Java guards run synchronously and fail fast; on success the download/subprocess work happens in a background thread.
Response (202): the current progress entry, e.g. { "active": true, "phase": "starting", ... }
Errors:
400no loader/version configured, unsupported loader, or a Java guard failed. Java failures includerequired_java,detected_java,java_missing,java_too_old,compatibilityandrecommended_install.409 {"error": "Another operation is in progress for this server"}
Poll install progress. active is true until phase becomes done or failed. A missing entry
also reports active: false, which is what you see if the backend restarted mid-install. The
frontend polls at roughly 750 ms intervals. Returns 404 for an unknown server.
{
"active": true,
"phase": "downloading_server_jar",
"bytes_done": 12058624,
"bytes_total": 48234496,
"server_id": "srv_a1b2c3d4",
"loader": "fabric"
}Phase vocabulary (set by the concrete loaders; treat as opaque strings elsewhere):
| Phase | Meaning |
|---|---|
starting |
worker thread spawned |
resolving_versions |
versions API call in flight |
downloading_installer |
installer JAR download (carries bytes_done/bytes_total) |
downloading_server_jar |
server JAR download (carries bytes_done/bytes_total) |
verifying |
SHA1 check |
running_installer |
subprocess execution; phase only, no bytes |
detecting_artifacts |
post-install file detection |
writing_eula |
final eula.txt write |
done |
completed successfully |
failed |
failed; the entry carries error |
Response (200): { "success": true, "message": "...", "server": { ... }, "compatibility": { ... } }
Errors:
400server stillpending(install first), currentlyinstalling, or a Java check failed — the body carriesjava_missing,java_too_old,required_java,detected_javaandrecommended_install.409a stop is still draining.
Always returns 200. If the server is running the status flips to stopping and the actual stop
runs in a background thread; poll the server record for the final state.
Response (200): { "success": true, "message": "...", "details": { ... }, "server": { ... } }
Error (400): restart could not complete; success is false.
Query: limit (int, default 200) — applied to each stream separately.
{
"stdout": [{ "ts": "2026-01-04T12:00:00Z", "text": "Done (7.481s)! For help, type \"help\"" }],
"stderr": [],
"running": true
}When no process is registered for the server, the response is
{ "stdout": [], "stderr": [], "running": false, "message": "Server is not running" }. This route
does not validate the server id, so an unknown id yields that same not-running body rather than a
404.
All paths are resolved relative to the server's install directory and are rejected if they escape it.
Query: path (string, optional) — relative subdirectory; omit for the install root.
{
"currentPath": "config",
"absolutePath": "/srv/servers/srv_a1b2c3d4/config",
"entries": [
{
"name": "fabric",
"size": 40960,
"updatedAt": "2026-01-04T12:00:00Z",
"path": "/srv/servers/srv_a1b2c3d4/config/fabric",
"relativePath": "config/fabric",
"isDir": true
}
]
}Directories are listed first, then files, both alphabetically. Directory sizes are the recursive sum of their contents.
Errors: 400 invalid path · 404 directory not found
Recursive name search over the install directory.
Query: q (string, required) — case-insensitive substring matched against entry names ·
path (string, optional) — restrict the search to a subdirectory · limit (int, optional,
default 200, max 500)
{
"query": "properties",
"scope": "",
"results": [
{
"name": "mod.properties",
"size": 1420,
"updatedAt": "2026-01-04T12:00:00Z",
"path": "/srv/servers/srv_a1b2c3d4/config/mod.properties",
"relativePath": "config/mod.properties",
"parentPath": "config",
"isDir": false
}
],
"truncated": false
}Symlinked directories are not followed. size is null for directories — unlike the browse
endpoint, search does not compute recursive directory sizes. truncated is true when the hit
limit was reached or the internal scan cap (200,000 entries) fired, meaning more matches may
exist.
Errors: 400 missing q or invalid path · 404 directory not found
Query: path (string, required)
Response (200): { "path": "server.properties", "content": "..." }
Errors: 400 path missing/invalid, or the file is not UTF-8 text · 404 file not found
Body: { "path": "server.properties", "content": "..." }
Response (200): { "success": true }
Errors: 400 path/content missing or invalid · 404 file not found · 500 write failed
List installed mod files, sorted by name. Same entry shape as the file browser.
Response (200): { "success": true, "message": "sodium.jar removed" }
Errors: 400 invalid path · 404 mod file not found
Bulk delete. Body: { "filenames": ["a.jar", "b.jar"] }
Response (200): { "success": true, "deleted": ["a.jar"], "errors": [{ "filename": "b.jar", "error": "File not found" }] }
Error (400): filenames missing or empty
Send a command to the running server's stdin.
Body: { "command": "say hello" }
Response (200): the registry result (success: true). Error (400): command missing, or the
registry rejected it (e.g. the server is not running).
{ "status": "running", "ram": 2147483648, "pid": 12345 }{
"cpu": { "percent": 12.4 },
"memory": { "percent": 48.2, "totalBytes": 16777216000, "usedBytes": 8087896064 }
}Error (500): psutil is not installed.
Lists the Minecraft and loader versions a registered loader can install. The frontend
(ServerCreateModal) uses these as soon as a loader is picked. Loaders registered in
LOADER_REGISTRY are exposed here automatically. Lookup is case-insensitive.
Registered today: fabric, forge, neoforge, quilt, paper, folia,
purpur, pufferfish, vanilla.
Supported Minecraft versions, in a normalized schema:
[
{ "version": "1.21.4", "stable": true, "type": "release" },
{ "version": "24w45a", "stable": false, "type": "snapshot" }
]stable is the only field every consumer can rely on. type is loader-native (e.g.
release/snapshot for Vanilla) and may be absent when a loader makes no such distinction.
Error (404): { "error": "Unknown loader: <name>" }
Loader-specific versions. Loaders without a separate loader version (e.g. Vanilla) return [].
Query: mc_version (string, optional) — filter to one Minecraft version.
Response (200): a loader-native array; the shape varies per loader and the frontend passes it through opaquely.
Fabric example:
[{ "loader": { "version": "0.16.0", "stable": true } }]Fabricator can download and manage its own Temurin runtimes alongside whatever Java is on PATH.
Set FABRICATOR_SKIP_JAVA_CHECK=1 to bypass enforcement.
Query: mc_version (string, optional) · required_java (int, optional) ·
java_path (string, optional, default java)
{
"required_java": 21,
"install_major": 21,
"system_java": { "path": "java", "version": 17, "meets_requirement": false },
"managed_java": { "path": "/home/u/.fabricator/java/21", "installed": true, "major": 21, "substituted": false },
"asset": {
"download_url": "https://api.adoptium.net/v3/binary/...",
"filename": "OpenJDK21U-jre_x64_linux_hotspot.tar.gz",
"size_bytes": 44000000,
"checksum_algorithm": "sha256",
"install_major": 21,
"substituted": false
},
"asset_error": null,
"arch": "x64",
"compatibility": { "required_java": 21, "enforceable": true },
"recommended_install": { "required_java": 21, "download_url": "...", "linux_install_command": "sudo apt install openjdk-21-jre-headless", "installer_type": "tar.gz", "arch": "x64" }
}The flat legacy fields (installed, version, detected_major, java_path, meets_requirement,
java_enforcement_skipped, platform, download_url, linux_install_command) are still present
for backward compatibility.
Start a managed Java install.
Body: { "major": 21 }
Response (200): { "task_id": "...", "status": "queued", "requested_major": 21, "install_major": 21, "substituted": false }
Error (400): major is not an integer, or is outside 8–99.
Poll an install task. Error (404): unknown task id.
Signal cancellation. Best-effort: the worker only notices between phases or download chunks.
Response (200): the updated task
Errors: 404 unknown task · 409 task already in a terminal state
{
"managed": [{ "major": 21, "path": "/home/u/.fabricator/java/21/bin/java", "version": 21 }],
"system": { "path": "java", "version": 17, "installed": true }
}Managed entries are sorted by major ascending and are removable; major is the directory name and
version is the major actually reported by the binary (null if the probe fails). The system entry
is informational only — it lives outside Fabricator's data directory.
Remove a managed runtime. Safe: if a server later needs it, the normal resolution flow prompts to reinstall.
Response (200): { "success": true, "major": 21 }
Errors: 400 invalid major · 404 no managed install for that major
Player names must match [A-Za-z0-9_]{1,16}. Ban/kick reasons must be a single line of ≤ 256
characters. When the server is running, changes are applied through console commands; when it is
stopped, the JSON files are edited directly.
Shared error responses: 409 when the operation needs a different server state ·
404 when the player is not in the list or is unknown to Mojang · 502 when the Mojang lookup fails.
whitelist, ops, bans, ipBans and knownPlayers are passed through verbatim from the
server's whitelist.json, ops.json, banned-players.json, banned-ips.json and usercache.json
— so their entries carry whatever vanilla writes, not a Fabricator-defined schema. A missing file
reads as [].
{
"whitelist": [{ "uuid": "...", "name": "Steve" }],
"ops": [{ "uuid": "...", "name": "Steve", "level": 4, "bypassesPlayerLimit": false }],
"bans": [],
"ipBans": [],
"knownPlayers": [],
"whitelistActive": false,
"enforceWhitelist": false,
"onlineMode": true
}Currently connected players, tracked from the server's stdout. Returns [] when the server is not
running.
[{ "name": "Steve", "uuid": "853c80ef-3c37-49fd-aa49-938b674adae6", "joinedAt": "2026-01-04T12:00:00+00:00" }]uuid is backfilled from the local Mojang cache when available and is null otherwise — this
endpoint makes no network calls.
Add or remove a whitelist entry. Body: { "name": "Steve" }
Toggle the whitelist at runtime. Body: { "active": true }
Persist the enforce-whitelist property. Only while stopped — returns 409 when running (use
whitelist/active instead). Body: { "active": true }
Response (200): { "enforceWhitelist": true }
Body: { "name": "Steve", "level": 4 } (level 1–4, default 4)
Vanilla's op command takes no level argument, so a running server returns 409 for any level
other than 4.
Change an op level. Body: { "name": "Steve", "level": 2 }
Minecraft only reads ops.json at startup, so this returns 409 while the server is running.
Body: { "name": "Steve" }
Body: { "name": "Steve", "reason": "griefing" } (reason optional; ignored on DELETE)
Body: { "ip": "192.168.1.10", "reason": "..." }
IPv4 or a wildcard such as 192.168.*. Invalid values return 400.
Running server only. Body: { "name": "Steve", "reason": "..." }
Backup work runs asynchronously: the endpoints return a job_id you poll via
GET /api/backup-jobs/<job_id>.
Each config is annotated with nextRunTime pulled live from the scheduler.
Body (name is required; everything else has a default):
{
"name": "Nightly",
"storagePath": "",
"maxSnapshots": 0,
"flush": true,
"shutdown": false,
"compress": true,
"exclusions": [],
"schedule": {
"enabled": true,
"frequencyHours": 24,
"timeOfDay": "03:00",
"timezone": "Europe/Berlin"
}
}An empty storagePath defaults to <install>/backups. maxSnapshots: 0 means unlimited. An empty
timezone falls back to the host zone; a non-empty one must be a valid IANA zone.
Response (201): the stored config, with defaults filled in and nextRunTime annotated.
{
"id": "bkc_dc4cb89bbf",
"serverId": "srv_4f16a5d0",
"name": "Nightly",
"storagePath": "",
"maxSnapshots": 0,
"flush": true,
"shutdown": false,
"compress": true,
"exclusions": [],
"schedule": { "enabled": false, "frequencyHours": 24, "timeOfDay": "03:00", "timezone": "" },
"nextRunTime": null,
"createdAt": "2026-07-17T09:59:23.953048Z",
"updatedAt": "2026-07-17T09:59:23.953048Z"
}Error (400): missing name (Field 'name' is required), non-integer or negative
maxSnapshots, frequencyHours <= 0, or an invalid timezone
Partial update — the same validation applies to whichever fields are present. 404 if unknown.
Query: purge (bool, optional) — when set, deletes archive files that are both inside the
config's effective storage path and recorded as snapshots of this config.
{
"success": true,
"config_id": "bkc_dc4cb89bbf",
"purge": true,
"deleted_files": 3,
"deleted_paths": ["..."],
"retained_files": 1,
"retained_paths": ["..."]
}The response always reports both lists — no silent orphans. Without purge, the files stay on disk
and all appear under retained_paths.
Unlinks the archive and removes the record. Errors: 404 unknown snapshot · 500 unlink failed
Query: format (tar default, or zip)
zip repacks the archive on the fly into a flat directory tree for browsers; the temp file is
cleaned up after the response.
Errors: 404 unknown snapshot, or the archive is missing on disk
Body: { "mode": "in_place" | "reset" }
Response (202): { "success": true, "job_id": "..." }
Errors: 400 invalid mode · 404 unknown snapshot
Run a configured backup now. Response (202): { "success": true, "job_id": "..." }
Ad-hoc backup without a stored config.
Body: { "storagePath": null, "compress": true, "flush": true, "shutdown": false } (all optional)
Response (202): { "success": true, "job_id": "..." }
{
"total_snapshots": 12,
"total_size_bytes": 4823449600,
"last_snapshot": { "id": "...", "createdAt": "..." },
"next_run": { "config_id": "bkc_dc4cb89bbf", "config_name": "Nightly", "next_run_time": "..." },
"configs_count": 2,
"defaultStoragePath": "/srv/servers/srv_a1b2c3d4/backups"
}Upload a world archive and replace the server's active world. The raw archive bytes are the request
body (fetch(url, { body: file })); the display name comes from the filename query parameter or
the X-Filename header.
Response (202): { "success": true, "job_id": "..." }
Errors: 400 empty upload or invalid archive · 413 upload exceeds the limit
(FABRICATOR_MAX_WORLD_UPLOAD_BYTES)
Poll any backup, restore or world-import job. Job ids are globally unique, so this route is not server-scoped.
Response (200): { "active": true, ... } · Error (404): unknown job
Rate limiting. Modrinth allows 300 requests/minute per IP. Every call Fabricator makes goes
through one process-wide token bucket (250/min, leaving headroom for anything else sharing the IP),
which also reads the X-Ratelimit-Remaining the API reports and pauses everyone on a 429 for as
long as Retry-After asks. Any endpoint below can therefore return:
Error (429): { "error": "...", "retry_after": 12.0 } — also sent as a Retry-After header.
retry_after is seconds; wait that long rather than retrying immediately.
Identify every .jar in the server's mods folder by content hash. One request covers the whole
folder — the backend hashes the jars and asks Modrinth's bulk version_files endpoint, then caches
the results (a file hash maps to one version permanently, so a page refresh costs nothing).
Use this instead of looking mods up by name: filenames are not reliable identifiers, and per-file lookups do not fit in the rate limit for a large modpack.
{
"resolved": {
"sodium-fabric-0.6.0+mc1.21.1.jar": {
"projectId": "AANobbMI",
"slug": "sodium",
"title": "Sodium",
"iconUrl": "https://cdn.modrinth.com/...",
"versionId": "xexnGRr6",
"versionNumber": "mc1.21.1-0.6.0"
}
}
}Jars Modrinth does not recognise — hand-modified, repackaged, or never published there — are simply
absent from resolved. A missing mods folder returns { "resolved": {} }.
Error (404): unknown server · (400): mods folder could not be resolved
Query: query (string) · mc_version (string, optional) · loader (string, optional) ·
project_type (mod default, or plugin — Bukkit-family servers browse plugins through this
same route; any other value falls back to mod) · limit (int, default 20) ·
offset (int, default 0) ·
index (downloads default, relevance, follows, newest, updated)
GET /api/modrinth/search?query=sodium&mc_version=1.20.1&loader=fabric&limit=10{
"hits": [
{
"project_id": "AANobbMI",
"slug": "sodium",
"title": "Sodium",
"description": "A modern rendering engine...",
"downloads": 90301924,
"icon_url": "https://cdn.modrinth.com/...",
"project_type": "mod",
"versions": ["1.20.1", "1.20.2"],
"client_side": "required",
"server_side": "optional"
}
],
"offset": 0,
"limit": 10,
"total_hits": 1
}Same parameters and response shape as /search, restricted to modpacks.
Project details. GET /api/modrinth/mod/<mod_id> is a legacy alias with identical behaviour.
{
"id": "AANobbMI",
"slug": "sodium",
"title": "Sodium",
"description": "The fastest and most compatible...",
"downloads": 90301924,
"categories": ["optimization", "fabric"],
"client_side": "required",
"server_side": "optional",
"versions": ["versionId1", "versionId2"],
"game_versions": ["1.20.1", "1.20.2"],
"loaders": ["fabric"]
}All versions of a project. GET /api/modrinth/mod/<mod_id>/versions is a legacy alias.
Query: loaders (repeatable) · game_versions (repeatable) · featured (bool, optional)
GET /api/modrinth/project/sodium/versions?loaders=fabric&game_versions=1.20.1[
{
"id": "versionId",
"version_number": "0.5.13",
"name": "Sodium 0.5.13 for Fabric",
"version_type": "release",
"date_published": "2025-03-03T17:45:49.132919Z",
"downloads": 12345,
"game_versions": ["1.20.1"],
"loaders": ["fabric"],
"files": [
{
"url": "https://cdn.modrinth.com/.../sodium-fabric-0.5.13+mc1.20.1.jar",
"filename": "sodium-fabric-0.5.13+mc1.20.1.jar",
"primary": true,
"size": 1234567,
"hashes": { "sha512": "...", "sha1": "..." }
}
]
}
]Resolve the best version for a target.
Query: mc_version (string, required) · loader (string, optional)
{
"project_id": "sodium",
"mc_version": "1.20.1",
"loader": "fabric",
"version": { "id": "...", "version_number": "0.5.13", "files": ["..."] },
"download_url": "https://cdn.modrinth.com/..."
}version is the complete Modrinth version object (the same shape /versions returns), not a
summary. loader echoes back whatever you sent, including null.
Errors: 400 mc_version missing · 404 no suitable version found
A single Modrinth version by id.
Direct download URL for the best matching version.
Query: mc_version (string, required) · loader (string, optional, default fabric)
{ "download_url": "https://cdn.modrinth.com/data/AANobbMI/versions/OihdIimA/sodium-fabric-0.5.13%2Bmc1.20.1.jar" }Errors: 400 mc_version missing · 404 no suitable version found
Download a mod and install it into the server's mods folder. The download is hash-verified.
Body: mc_version (string, required) · server_id (string, required) ·
loader (string, optional, default fabric)
{ "mc_version": "1.20.1", "server_id": "srv_a1b2c3d4", "loader": "fabric" }Response (200):
{
"success": true,
"message": "Mod installed successfully",
"file": "sodium-fabric-0.5.13+mc1.20.1.jar",
"path": "/srv/servers/srv_a1b2c3d4/mods/sodium-fabric-0.5.13+mc1.20.1.jar"
}Errors: 400 server_id or mc_version missing, or a mods_folder override was sent (no
longer allowed) · 404 server not found, or no suitable version · 409 another operation holds
the server lock
Install a modpack into a server. The server must be stopped.
Body: server_id (string, required) · mc_version (string, optional) ·
loader (string, optional) · clean_install (bool, default true) ·
create_backup (bool, default true) · allow_missing (bool, default false) ·
mod_side_overrides (object, optional)
Response (200): the install result plus success, message, backup_file (when a backup was
made) and java_warning (when the pack's Minecraft version needs a newer Java than the one detected).
Errors: 400 server running, or install path unresolvable · 404 server not found ·
409 an install is already in progress · 500 backup or install failure
Stage a .mrpack exported from the Modrinth app. Nothing is installed — the response describes
what the pack declares, so a server can be created to match it before the pack is installed.
Body: the raw archive bytes (Content-Type: application/octet-stream). The display filename
comes from the filename query param or the X-Filename header.
Response (201):
{
"success": true,
"upload_id": "8f14e45fceea167a5a36dedd4bea2543",
"filename": "my-pack.mrpack",
"size_bytes": 41233,
"name": "My Pack",
"version": "1.2.3",
"summary": "...",
"minecraft_version": "1.20.1",
"loader": "fabric",
"loader_version": "0.15.7",
"file_count": 118,
"client_only_count": 24,
"dependencies": { "minecraft": "1.20.1", "fabric-loader": "0.15.7" },
"has_overrides": true,
"has_server_overrides": false
}Staged uploads expire after 6 hours and are swept on the next upload.
Errors: 400 empty body, not a zip, no modrinth.index.json, malformed index, or a pack for
another game · 413 over FABRICATOR_MAX_MRPACK_UPLOAD_BYTES (default 2 GiB), or expanding past
FABRICATOR_MAX_MRPACK_EXTRACTED_BYTES (default 16 GiB)
Install a staged .mrpack onto a server. The server must be stopped. Same install semantics as
/modpack/<project_id>/install — side classification, overrides, the missing-file 409 and the
non-blocking uncertain_mod_files report all behave identically.
Body: server_id (string, required) · loader (string, optional — defaults to what the
pack declares) · clean_install (bool, default true) · create_backup (bool, default true) ·
allow_missing (bool, default false) · mod_side_overrides (object, optional) ·
force (bool, default false)
The upload survives a failed install so the missing-file retry can reuse it; a successful install consumes it.
Errors: 400 server running, or install path unresolvable · 404 server not found, or the
upload expired · 409 an install is already in progress; missing files; or — without force —
the pack targets a different Minecraft version or loader than the server runs, in which case the
body carries can_continue_with_mismatch, pack_mc_version, pack_loader, server_mc_version,
server_loader and reasons
Discard a staged upload. Errors: 404 unknown or already-consumed upload.
Response (200): { "active": true, "stage": "downloading", "current": 12, "total": 80, "detail": "..." }
or { "active": false } when nothing is running.
[{ "icon": "...", "name": "adventure", "project_type": "mod", "header": "Adventure" }][{ "icon": "...", "name": "fabric", "supported_project_types": ["mod", "modpack"] }][{ "version": "1.20.1", "version_type": "release", "date": "2023-06-12T00:00:00Z", "major": true }]Upstream Modrinth failures are surfaced with the upstream status code (or 502 when there is none).
Exposes a server publicly through a playit.gg tunnel. Not supported on Windows — every endpoint
then reports status: "unsupported".
{
"status": "running",
"claim_url": null,
"error_reason": null,
"binary_trust": "verified",
"tunnels": [
{
"local_port": 25565,
"address": "example.gl.at.ply.gg:12345",
"disabled_reason": null,
"name": "my-tunnel",
"tunnel_type": "minecraft-java"
}
],
"tunnels_known": true
}status is daemon/account level and is one of unsupported, stopped, provisioning, claiming,
starting, running, error. error means a real account/daemon failure — a per-tunnel
disabled_reason is not a global error and rides on the tunnel instead. provisioning appears on
the first start of an install that has no playit binaries yet: Fabricator downloads the pinned
release itself, then continues into the normal claiming/starting sequence.
binary_trust describes the binaries currently on disk, computed by hashing them against the pinned
release (it replaces the former binary_verified boolean, which was declared once at install time
and was never set at all for Docker installs):
| Value | Meaning |
|---|---|
verified |
Both binaries match the release Fabricator pins. |
unverified |
A binary in a Fabricator-managed directory does not match the pin — worth surfacing. |
system |
The binaries came from the operator's own install (e.g. a distro package, which discovery prefers). Not verified, and not a fault. |
missing |
Nothing installed. error_reason covers it once a start is attempted. |
Derive a server's public address by matching tunnel.local_port to that server's port; address
may be null. "No matching tunnel" is a per-server hint, not an error. tunnels_known stays
false until the first successful poll, so callers can tell "zero tunnels" apart from "not
reachable yet"; both leave tunnels empty.
Each performs the action and returns the full post-action status, so no follow-up poll is needed.
Current updater state plus the latest-version check. selfUpdateDisabled is true when the
deployment manages updates out-of-band (the container image sets FABRICATOR_DISABLE_SELF_UPDATE=1).
Start an asynchronous self-update.
Body: { "version": "1.2.3" } — optional; a release tag or latest.
Response (202): { "started": true, "requestedVersion": "1.2.3" }
Errors:
403self-update is disabled for this deployment (FABRICATOR_DISABLE_SELF_UPDATE=1).409the update did not start. The route maps every unstarted outcome to409, so this covers an update already in progress, an invalid version string, and a failure to queue the request — readerrorin the body to tell them apart. Note that an invalid version yields409here, not400.
Set these in .env or the environment.
# Flask
FLASK_ENV=development # or 'production'
HOST=127.0.0.1 # loopback by default; set explicitly for remote access
PORT=5000
SECRET_KEY=... # session signing key; auto-generated and persisted if unset
# CORS — comma-separated allowlist. '*' is rejected; entries need an http(s) scheme and a host.
CORS_ORIGINS=http://localhost:3000
# Storage (defaults: %APPDATA%\Fabricator on Windows, ~/.fabricator on POSIX;
# /var/lib/fabricator/... under FLASK_ENV=production)
SERVER_ROOT=... # server install directories
SERVER_INDEX_FILE=... # servers.json
JAVA_ROOT=... # managed Java runtimes
BACKUPS_DIR=... # backup archives
FABRICATOR_APPDATA=... # override the whole data directory
# Authentication
FABRICATOR_DISABLE_AUTH=1 # turn off the built-in login entirely
FABRICATOR_AUTH_PASSWORD_HASH=... # declare the password out-of-band (blocks /change-password)
FABRICATOR_SESSION_COOKIE_SECURE=1
# Server process
SERVER_COMMAND="java -Xmx4G -jar server.jar nogui" # fallback launch command
# Behaviour toggles
FABRICATOR_SKIP_JAVA_CHECK=1 # bypass Java version enforcement (dev/testing only)
FABRICATOR_DISABLE_SCHEDULER=1 # do not boot the backup scheduler
FABRICATOR_DISABLE_SELF_UPDATE=1 # make POST /api/system/update return 403
FABRICATOR_MAX_WORLD_UPLOAD_BYTES=... # world-import cap; default 10 GiB
FABRICATOR_MAX_MRPACK_UPLOAD_BYTES=... # .mrpack upload cap; default 2 GiB
FABRICATOR_MAX_MRPACK_EXTRACTED_BYTES=... # .mrpack uncompressed-size cap; default 16 GiB
FABRICATOR_MANAGED=1 # managed-hosting mode: lock deployment-owned controls (third-party hosts only)
FABRICATOR_MANAGED_MEMORY_GB=... # managed mode: pinned per-server JVM heap in whole GB; required when managed
# Self-updater
FABRICATOR_REPO=philderks/Fabricator
FABRICATOR_UPDATE_MODE=...
FABRICATOR_UPDATE_DIR=/var/lib/fabricator/update
# playit.gg
PLAYIT_ENABLED=true # must be the literal 'true' (see below)
PLAYIT_RUNTIME_DIR=... # writable dir for the agent's secret and, if the binaries are
# missing, the ones Fabricator downloads (<dir>/bin)PLAYIT_BINARY_VERIFIED is no longer read. binary_trust is now computed by hashing the
binaries on disk against the pinned release, so a value asserted once at install time can no longer
go stale. tools/install.sh still writes the variable into the env file; it is inert.
Truthiness is not uniform. Most flags (FABRICATOR_DISABLE_AUTH, FABRICATOR_SKIP_JAVA_CHECK,
FABRICATOR_SESSION_COOKIE_SECURE, FABRICATOR_MANAGED) go through bool_from_str, which accepts
1, true, yes or on, case-insensitively. But FABRICATOR_DISABLE_SCHEDULER and FABRICATOR_DISABLE_SELF_UPDATE
match the literal string 1 only, and PLAYIT_ENABLED matches the literal
lowercase true only — PLAYIT_ENABLED=1 does not work.
PLAYIT_ENABLED is only a fallback: the state file written by the dashboard toggle wins when present.
SECRET_KEY and FABRICATOR_AUTH_PASSWORD_HASH are sensitive — never log them.
In backend/core/config.py. get_config() builds a fresh instance per call, so environment
overrides always take effect (no import-time snapshotting).
DevelopmentConfig— debug on; data under the user's appdata directory.ProductionConfig— debug off; data under/var/lib/fabricator.
The loader dispatch layer lives in backend/server/installer/__init__.py. Adding a loader takes
three steps:
-
Subclass
InstallerBase(backend/server/installer/base.py) and implement:loader_name(property) — the registry key, e.g."neoforge"get_minecraft_versions()→List[{version, stable, type?}]in the normalized schema (see/api/loaders/<loader>/versions/game)get_available_versions(mc_version)→ loader-native array;[]when the loader has no separate loader versioninstall(mc_version, loader_version=None)→InstallResultwithlaunch: LaunchSpecset. TheLaunchSpecis persisted inservers.jsonand drivesServerProcessRegistry._build_command
-
Register it in
LOADER_REGISTRY:from .neoforge import NeoForgeInstaller LOADER_REGISTRY: Dict[str, Type[InstallerBase]] = { "fabric": FabricInstaller, "vanilla": VanillaInstaller, "neoforge": NeoForgeInstaller, "quilt": QuiltInstaller, "forge": ForgeInstaller, "paper": PaperInstaller, "folia": FoliaInstaller, "purpur": PurpurInstaller, "pufferfish": PufferfishInstaller, }
get_installer_for(loader, install_path)(case-insensitive) andsupported_loaders()pick up the new entry automatically — routes and the install flow need no changes. -
Frontend: add an option to
loaderOptionsinfrontend/src/components/modals/ServerCreateModal.vueusing the samevalueasloader_name.
If your installer needs Java at install time (not just at runtime), set
requires_java_for_install so the install route runs the extra Java guard.
_build_command raises ValueError on an unknown LaunchSpec.type — that is deliberate, so a
record written by a newer build is caught early on an older one.
ModrinthClient can be used directly:
from pathlib import Path
from backend.modrinth.client import ModrinthClient
client = ModrinthClient()
results = client.search(project_type="mod", query="sodium", mc_version="1.20.1", loader="fabric")
resolved = client.get_project_download_url(project_id="sodium", mc_version="1.20.1", loader="fabric")
file = client.download_mod(resolved["url"], Path("./mods"), hashes=resolved["hashes"])Key methods:
search(project_type, query, mc_version, loader, limit, offset, index)— search projectsget_project(project_id)— project detailsget_project_versions(project_id, loaders, game_versions, featured)— versionsget_version(version_id)— a single versionresolve_project_version(project_id, mc_version, loader)— best matching version + URLget_project_download_url(project_id, mc_version, loader)— resolve straight to a URL + hashesdownload_mod(url, target_folder, hashes=...)— hash-verified downloadinstall_modpack(project_id, install_path, ...)— resolve a Modrinth pack, download, installinstall_mrpack_archive(mrpack_path, install_path, ...)— install an on-disk.mrpack
Failures raise ModrinthApiError, which carries the upstream status_code and any details.
Authentication is on unless you set FABRICATOR_DISABLE_AUTH=1, so log in first and reuse the
session cookie. The default base URL is http://localhost:5000.
# Log in and store the session cookie
curl -c cookies.txt -X POST http://localhost:5000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"password": "your-password"}'# 1. Create the record (returns 201 with the new id, e.g. srv_a1b2c3d4)
curl -b cookies.txt -X POST http://localhost:5000/api/servers \
-H 'Content-Type: application/json' \
-d '{"name": "Survival", "version": "1.20.1", "loader": "fabric",
"port": 25565, "installPath": "/var/lib/fabricator/servers/survival"}'
# 2. Lay down the files (returns 202 — the work runs in the background)
curl -b cookies.txt -X POST http://localhost:5000/api/servers/srv_a1b2c3d4/install
# 3. Poll until "active": false
curl -b cookies.txt http://localhost:5000/api/servers/srv_a1b2c3d4/install/progress
# 4. Start it
curl -b cookies.txt -X POST http://localhost:5000/api/servers/srv_a1b2c3d4/startcurl -b cookies.txt "http://localhost:5000/api/modrinth/search?query=create&mc_version=1.20.1&loader=fabric"
# server_id is required — the mods folder is resolved from it
curl -b cookies.txt -X POST http://localhost:5000/api/modrinth/mod/create-fabric/install \
-H 'Content-Type: application/json' \
-d '{"mc_version": "1.20.1", "loader": "fabric", "server_id": "srv_a1b2c3d4"}'# Ad-hoc backup, no stored config needed (returns 202 + job_id)
curl -b cookies.txt -X POST http://localhost:5000/api/servers/srv_a1b2c3d4/backup-quick \
-H 'Content-Type: application/json' -d '{"compress": true, "flush": true}'
# Poll the job
curl -b cookies.txt http://localhost:5000/api/backup-jobs/<job_id>curl -b cookies.txt http://localhost:5000/api/servers/srv_a1b2c3d4/metrics
curl -b cookies.txt "http://localhost:5000/api/servers/srv_a1b2c3d4/logs?limit=50"
curl -b cookies.txt -X POST http://localhost:5000/api/servers/srv_a1b2c3d4/console \
-H 'Content-Type: application/json' -d '{"command": "say hello"}'pytest- Authentication: single-operator password login, enabled by default; disable with
FABRICATOR_DISABLE_AUTH=1. The/api/gate is default-deny. - CORS: explicit allowlist only —
*is rejected at startup. - Path traversal: file, mod and backup paths are resolved and rejected if they escape the server's install directory.
- Self-update: gate it with
FABRICATOR_DISABLE_SELF_UPDATE=1when the panel is exposed and updates come from elsewhere. - Modrinth rate limits: the client sets an identifying
User-Agentas Modrinth's policy requires, but it does not throttle, back off, or retry. Requests go out as fast as callers make them, and a429from Modrinth is surfaced to you unchanged (ModrinthApiError→ HTTP429) rather than being retried. Modrinth's documented limit is 300 requests/minute; staying under it is the caller's responsibility. - Outbound timeouts: Modrinth calls use a 15-second timeout.
GNU Affero General Public License v3.0 (AGPL-3.0). Copyright © 2026 Philipp Noél Derks and Linus Sommermeyer.
AGPL-3.0 is a network copyleft licence: if you run a modified Fabricator as a network service, you must offer its users the corresponding source of your modified version.
Bug reports and pull requests are welcome — see CONTRIBUTING.md for the full process. In short:
- Bugs and small self-contained fixes: open an issue; a PR alongside it is fine.
- New features, refactors, restructuring, build/CI/packaging changes: open an issue and wait for agreement before writing code. Unsolicited structural PRs are likely to be declined.
- Security vulnerabilities: do not open a public issue or PR — report privately.
- New behavior should come with a test; a bug fix should come with a test that fails before the fix.
pytestis already inrequirements.txt, so there is no separate dev install.
Opening a pull request means agreeing to the Contributor License Agreement in CONTRIBUTING.md: contributions are licensed to the public under AGPL-3.0, and you also grant the maintainers a perpetual, royalty-free licence to relicense them, including commercially. The CLA Assistant bot records your acceptance once per GitHub account.
- GitHub Issues: Fabricator Issues
- Modrinth API docs: docs.modrinth.com
Beta. This feature is young and its client-side setup may change between releases. The machine running your AI client needs
uvinstalled.
Fabricator can issue scoped API tokens so an MCP client — an AI assistant such as Claude — can read your server's state and perform a small set of maintenance actions over the same HTTP API this document describes. The typical use is modpack crash diagnosis: read the crash log, list the installed mods, check a mod against Modrinth, remove or update it, restart.
The feature is off by default. Nothing changes for your panel until you turn it on and mint a token.
Open Settings in the panel sidebar and find the Model Context Protocol panel.
- Turn Enable MCP access on.
- Choose Create token, give it a name and a scope (
readormanage). - Copy the token immediately — it is shown once and cannot be retrieved afterwards. Only a SHA-256 hash of it is stored.
Revoke a token from the same panel; revocation takes effect on the next request.
Tokens look like fab_<id>_<secret>. The <id> is a non-secret lookup key that appears in the
token list; the <secret> half is what is never shown again.
| Scope | Can do |
|---|---|
read |
Read-only routes: server list and details, logs, installed mods, metrics, Java status, backups and snapshot listings, the Modrinth catalog. |
manage |
Everything read can do, plus start / stop / restart, re-install the configured loader, install or update a mod by Modrinth ID, and remove installed mods. |
Every /api route is classified into exactly one of three buckets, and the classification is
enforced by the panel itself, not by the client: 32 read, 7 manage, 58 never. Routes in
the never bucket are refused for every token regardless of scope — the console, all file read
and write routes, server settings, autostart, server create and delete, Java installation, the
self-updater, playit, backup restore/download/delete, world import, whole-modpack install (including
the .mrpack upload routes), all player administration (bans, kicks, ops, whitelist), the
player-data reads (they contain player names, UUIDs and IP addresses), and the token-management
routes themselves. A token can never mint a token, revoke one, or flip the switch.
Requests are answered with 401 when the credential itself is not accepted (bad, revoked or
expired token, or the switch is off) and 403 when the token is valid but the route is out of its
scope or in the never bucket. A 403 is a fact about your configuration, not a temporary
condition — retrying it will not change the answer.
Responses on the token path are filtered server-side before they leave the panel:
- Credential fields are removed — for example the RCON password stored in a server record.
- Host filesystem paths are removed, including absolute paths embedded in error messages. A path is reported relative to the panel's own directories, so a failure still names the file it failed on without disclosing your machine's layout.
GET /api/java/statusrefuses a caller-suppliedjava_path, because that value names a binary the panel would execute.
None of this applies to your own logged-in session: the operator sees the panel exactly as before.
These four routes are session-only — they require a logged-in operator and are refused to every token:
| Method | Route | Purpose |
|---|---|---|
GET |
/api/integrations/mcp |
Switch state and token metadata (never a secret) |
PUT |
/api/integrations/mcp |
{"enabled": bool} — turn MCP access on or off |
POST |
/api/integrations/mcp/tokens |
{"name": str, "scope": "read"|"manage"} — mint; the response is the only time the token is shown |
DELETE |
/api/integrations/mcp/tokens/<token_id> |
Revoke |
Using a token is the standard bearer scheme:
curl -H "Authorization: Bearer fab_xxx_yyy" http://localhost:5000/api/servers- Turning the switch off does not delete your tokens. They stop being accepted (
401) and start working again when you turn it back on. - Before the panel has a password (first-run setup), no token can be minted or used.
- Managed installs hide the Model Context Protocol panel entirely.
- Tokens are stored in the same
0600auth.jsonas your password hash.
- Server logs contain text written by mods and by players, and a
managetoken lets an assistant act on what it reads there. Treat amanagetoken as you would giving someone else the panel: prefer areadtoken for pure diagnosis, and hand outmanageonly when you want the assistant to be able to change something. - The token is stored in plain text in your MCP client's configuration file. Treat that file like a password.
- Tokens carry no expiry. Revoke any token you no longer use.
The MCP server that consumes these tokens lives in mcp/ in this repository. It runs on
your machine, next to your assistant, and talks to the panel over the API documented above.
It needs uv on your PATH — the one hard prerequisite, and not
something MCP clients bundle. If uv is missing, your client reports the server as failing to
launch or disconnecting at startup, which looks like a broken server rather than a panel or
token problem, because the process never starts at all.
Put this in your client's MCP configuration (the Settings panel generates it with your URL and token filled in):
{
"mcpServers": {
"fabricator": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/philderks/Fabricator@dev#subdirectory=mcp",
"fabricator-mcp"
],
"env": {
"FABRICATOR_URL": "http://127.0.0.1:5000",
"FABRICATOR_TOKEN": "YOUR_TOKEN_HERE"
}
}
}
}The package is installed from the repository because it is not on PyPI yet; when it is, command
and args become "uvx" and ["fabricator-mcp"] and nothing else changes.
The token goes in env, never in args — a command line is readable by every process on the
machine and is written to shell history.
| Scope | Tools |
|---|---|
read |
list servers · read logs · list and identify installed mods · CPU/memory · Java version checks · install progress and failure reasons · Modrinth search, project info and compatibility checks |
manage |
all of the above, plus start/stop/restart, install or update a mod by Modrinth id, and delete mod jars |
read is the recommended default: it answers every diagnostic question and cannot change
anything. The tool set is deliberately narrower than what a token may reach, and it is not a
security boundary — the panel is. Tools are never hidden based on scope; a manage tool called
with a read token returns the panel's 403 and the client says so.
Mods in subfolders: only jars directly in a server's mods folder are listed, and only those
can be removed through the MCP server. A jar in a subfolder must be managed in the panel UI.