Skip to content

mgctl, the Moongate tool

mgctl is the one tool that ships beside mgserver, the server, in release archives and Docker images; the Linux installer links it as the mgctl command. Releases up to 0.11 named the server Moongate.Server. Releases 0.7 to 0.11 named the tool mgboot, which only prepared the root, and shipped the migrations and the converter as two more executables, migration-runner/Moongate.MigrationRunner and mg-uoxconv; there, read mgboot <root> for mgctl init <root>.

Command What it does
mgctl init <root> Prepares a server root; this page. mgctl <root> does the same
mgctl migrate status|apply --target auth|world Lists or applies the versioned SQL; see Persistence migrations
mgctl plugin install|list|remove Installs, lists or removes plugin packages; see Install a plugin package
mgctl run Runs the server and starts it again after a restart or a crash; see Run the server
mgctl completion bash|zsh|fish Prints the script that completes mgctl with TAB; see TAB completion

mgctl --help lists the commands and mgctl <command> --help the options of one.

The data converters (UOX3 and ModernUO) are not part of mgctl any more: they are a Python tool, moongate-convert; see Migrate from UOX3.

mgctl completion <shell> prints a completion script for bash, zsh or fish. With it, TAB completes the commands (mgctl mi → migrate), the second word of migrate, the options of the command and what follows them: directories after --root-directory and the like, auth or world after --target, and a directory for the root of init.

The Linux installer puts the scripts where the shells look for them, so a new shell completes mgctl with nothing to do. Elsewhere, load it yourself:

Terminal window
eval "$(mgctl completion bash)" # bash: add the line to ~/.bashrc
source <(mgctl completion zsh) # zsh: add the line to ~/.zshrc, after compinit
mgctl completion fish | source # fish: or save it as ~/.config/fish/completions/mgctl.fish

The bash line uses eval because the bash 3.2 of macOS loads nothing from source <(...); there, a path with a space is completed without its quoting.

The fish script is generated from the same list of commands as the other two but, unlike them, is not exercised by the tests.

A plugin published as a .nupkg package, such as the ones of moongate-plugins, is installed with mgctl plugin, without unpacking it by hand. The guide for who runs a shard, with configuration, update and removal, is Install and manage plugins; this section lists what the commands check. Stop the server first: it reads plugins/ once, at start.

Terminal window
mgctl plugin install Moongate.Plugins.OpenAi --root-directory /srv/moongate
mgctl plugin install MyPlugin --repo myuser/my-plugins --root-directory /srv/moongate
mgctl plugin install ./Moongate.Plugins.OpenAi.0.2.1.nupkg --root-directory /srv/moongate
mgctl plugin install https://codeberg.org/moongate/moongate-plugins/releases/download/Moongate.Plugins.OpenAi-v0.2.1/Moongate.Plugins.OpenAi.0.2.1.nupkg
mgctl plugin list --root-directory /srv/moongate
mgctl plugin remove Moongate.Plugins.OpenAi --root-directory /srv/moongate

install takes a package file, an https address (a plain http one is refused, except for this machine, and a redirect must follow the same rule) or a plugin id, which it looks up in a repository: see Install by id from a repository. It does these steps in order, and stops at the first that fails, leaving the installed plugin as it was:

  1. Reads the nuspec: the package type must be MoongatePlugin and the id a plain folder name (letters, digits and ., -, _ between them; no device names such as CON).
  2. Checks the exact dependency on Moongate.Server.Core or Moongate.Hosting (a plugin may name either or both, at one version): it must be the version of this mgctl, which is the version of the server beside it. A plugin built for another version is refused here, before it can fail to load at start.
  3. Checks the bundle: at most 1000 files, no entry that leaves the bundle folder, and the <id>.dll and <id>.deps.json the loader needs.
  4. Shows the id, the version, the Moongate version it was built for and the SHA-256 of the package, and asks Install it? [y/N]. --yes skips the question. A plugin runs inside the server with all its permissions: install it only from a source you trust.
  5. Unpacks only the plugin/ folder of the package, at most 100 MB, into a work folder inside plugins/ whose name starts with a dot, then replaces <root>/plugins/<id>/ with it. Installing a newer version over an older one is how a plugin is updated; the nuspec stays in the folder.

A work folder that an interrupted install left is skipped by the server and deleted by the next install; a -old one holds the bundle that was being replaced, and the install says where it is.

list shows each folder of plugins/ that does not start with a dot, with its version, the Moongate version it was built for and where it came from: package for an install of mgctl, by hand for a bundle copied there, whose version is read from its assembly. remove <id> deletes the folder. The database tables of the plugin stay: migrations only go forward, so drop them yourself if you want them gone.

The root comes from --root-directory, then MOONGATE_ROOT, then the directory of mgctl, as for migrate; it must hold config/moongate.toml, so a mistyped path is refused instead of getting a plugins/ folder. Nothing is installed or changed on the server’s behalf: there is no automatic update; the plugin and the repository are the ones you name.

With a plugin id instead of a file, install looks the plugin up in a repository and installs the newest build made for the Moongate version of this mgctl. An argument that ends in .nupkg, names an existing file or starts with http:// or https:// is a package, not an id; anything else that is a valid id is looked up, so a mistyped file name is searched for in the repository.

Terminal window
mgctl plugin install Moongate.Plugins.OpenAi # the community repository
mgctl plugin install MyPlugin --repo myuser/my-plugins # releases of a Codeberg repository
mgctl plugin install MyPlugin --repo https://codeberg.org/myuser/my-plugins # the same, as an address
mgctl plugin install MyPlugin --repo https://github.com/myuser/my-plugins # releases of a GitHub repository
mgctl plugin install MyPlugin --repo ./my-packages # a folder of .nupkg files
mgctl plugin install MyPlugin --repo myuser/my-plugins --version 1.2.0 # an exact version
--repo Where it looks
not given moongate/moongate-plugins
a folder the <id>.<version>.nupkg files in it; a folder written like owner/name needs ./ in front, or it is taken for a Codeberg repository
owner/name (always on Codeberg), https://codeberg.org/owner/name or https://github.com/owner/name the published releases (the newest 500), for assets named <id>.<version>.nupkg (the name dotnet pack gives); the tag does not matter
  • Versions are tried newest first, and the first whose package says it was built for this Moongate wins; at most the 10 newest are opened, and the answer says so when more exist. When none is, the command lists the versions it found and the Moongate each was built for. A package whose own nuspec names another id or another version than its file name is skipped.
  • Prereleases (2.0.0-beta.1, and the releases marked as prereleases) and drafts are never picked by themselves. --version names one version, a prerelease included, and is refused like a file when it was built for another Moongate.
  • --repo and --version go with an id only; with a file or an address they are refused.
  • The prompt names the host and the repository beside the id, the version and the SHA-256. A repository is as trustworthy as its owner: the checks above do not make a plugin safe.
  • CODEBERG_TOKEN (or GITHUB_TOKEN for a repository on GitHub), from the environment and never from the command line, reaches a private repository and raises the API limit. It goes to the API host only, never to the host a download redirects to, and an asset address outside the API’s origin is ignored while a token is set. MGCTL_RELEASES_API_URL points mgctl at another releases API, such as a mirror or another Forgejo instance; it replaces the API of the host the repository is on, and that host’s token goes with it.

mgctl run starts mgserver as a child process and keeps it up:

mgctl run --root-directory ~/moongate
mgctl run --root-directory ~/moongate --log-level Debug
The server exits with mgctl run
0 (a shutdown, Ctrl+C) exits with 0
75 (a restart) starts it again at once
anything else starts it again after 1, 2, 4… up to 30 seconds

Five failures in a row, each within 60 seconds of its start, end the launcher with exit code 1, so a bad configuration cannot loop forever; a run that lasts a minute is healthy and resets the count.

Option Meaning
--root-directory <path> The server root; MOONGATE_ROOT when absent. Also given to the server
--no-init Do not run mgctl init on the root before each start
--server-path <file> The mgserver to run; by default the one beside mgctl
--restart-delay <seconds> The first wait after a crash, 0 to 30 (default 1)

Everything else on the line, with or without a -- before it, goes to mgserver unchanged.

Before each start the launcher prepares the root as mgctl init does, so a restart picks up the data files a new build ships; it prints nothing when that works and what init said when it fails. The server keeps your terminal: the prompt, TAB completion and history work as without the launcher.

Stopping. Ctrl+C reaches the server through the terminal and the launcher ends with it. A SIGTERM or SIGHUP sent to the launcher alone (docker stop, kill, systemd with KillMode=mixed) is passed on to the server once, and the launcher waits for it: the final save of a large world can take a minute or two, and a stop is never restarted, whatever the server answers. The launcher ends with the last exit code of the server. A closing terminal sends SIGHUP to the server too, which does not handle it: close the session with a shutdown, not by closing the window. On Windows nothing is forwarded; the console delivers Ctrl+C to both processes.

mgctl init prepares a Moongate data directory without starting the server or connecting to PostgreSQL. It is step 1 of the first-start sequence.

Terminal window
mgctl init /absolute/path/to/moongate-data

The root directory is the single required positional argument. Relative paths are resolved from the current working directory. Quote paths containing spaces:

Terminal window
mgctl init "/srv/my realm"
mgctl init --help

mgctl <root>, without init, is the older spelling and still works for a root written as a path (/srv/moongate, ./data) or naming a directory that exists; a bare new name such as mgctl data is refused, so a mistyped command never becomes a root. A command line that names no command, such as mgctl migrate alone, exits with code 2.

On Windows, use mgctl.exe init C:\MoongateData from the extracted distribution. Keep mgctl and mgserver from the same release together. When preparing a root, mgctl shows the same Moongate banner, version and codename as the server, followed by Root setup. Help and version output omit the banner, and so does mgctl init <root> --no-header: scripts/run_server.sh uses it, since the server it starts right after shows the banner itself.

Terminal window
mgctl init /srv/moongate --generate-admin-certificate \
--admin-certificate-hosts "login.example.test,192.0.2.10"

The CLI uses ConsoleAppFramework. --generate-admin-certificate is an optional presence flag. --admin-certificate-hosts is one comma-separated string and requires that flag. Omit the hosts option for local development: every certificate includes localhost, 127.0.0.1 and ::1 automatically. Additional entries must be DNS names or IP addresses, without URL schemes, ports or wildcard addresses. Use the names clients connect to; *, 0.0.0.0 and :: are bind addresses, not certificate identities.

The operation runs offline and creates a self-signed RSA server certificate valid for one year. It writes these files under the root:

File Contents
certificates/admin.pfx Server certificate and private key; no password; owner read/write only on Unix
certificates/admin.crt Public PEM certificate to trust in administration clients

It updates these four settings in config/moongate.toml:

[admin_api]
enabled = true
allow_insecure_loopback = false
certificate_path = "certificates/admin.pfx"
certificate_password = ""

The existing port, listen address, other settings and comments are preserved. A missing section is added; existing inline or dotted admin_api definitions must first be converted to an explicit [admin_api] section. A custom certificate path is never replaced implicitly: clear that setting explicitly only if you intend to switch identities.

No port opens during setup. The API starts at the next normal server startup. The default bind remains 127.0.0.1:2590; for private-network access, configure a reachable interface or listen_address = "*" and include the actual client-facing DNS/IP in the certificate hosts. See Administration API.

Rerunning the same command reuses a matching, unexpired certificate pair. It fails without overwriting that pair if either file is missing, invalid, expired, mismatched or lacks a requested name. There is no implicit renewal or rotation. For deliberate replacement, stop the server, move both existing certificate files to a protected backup location, rerun generation and update client trust before restarting. A passwordless PFX still contains the private key: keep it server-side and restrict access. On Windows, use an appropriate ACL for the service account. Distribute only admin.crt; clients must verify trust and hostname.

Path under the root Purpose
config/moongate.toml Current server defaults serialized as snake_case TOML; plugin sections such as [ultima] are appended at the first server start
logs/, plugins/ Standard server directories
migrations/auth/ The core auth SQL files included in the distribution
migrations/world/ The core World SQL file included in the distribution: the mobiles, items, world state, jail, bulletin board, book attachment, help page and guild tables
data/ The shard data files included in the distribution: maps, regions, races, skills, messages and the rest; see Shard data files
templates/ The item, loot, mobile, book, shop and other templates included in the distribution; see Templates
scripts/ The example mobile and item scripts included in the distribution: the mobiles/ and items/ scripts (for example mobiles/wander.lua and items/potion.lua) with the shared common/ modules and the events/, gumps/ and skills/ scripts; the engine writes definitions.lua and .luarc.json here at startup
.mgctl.lock Retained file used to prevent simultaneous initialization

The new config sets persistence.migrations_directory to the absolute migrations path inside this root. Normal server startup also defaults to <root>/migrations when that setting is absent; keeping the explicit path makes mgctl migrate use the same catalog. Other values remain the server defaults: automatic migration generation and automatic schema synchronization are disabled. Runtime Redis credentials must be supplied before starting the server. The root does not need database access or Ultima Online client files to be prepared.

Data files, templates and scripts are copied only when missing, so a file you edited stays as it is. Run mgctl init again after upgrading to add the files a new release introduces; a file that exists in the root is never replaced, so compare it with the one beside the new mgserver binary (data/, templates/, scripts/) to pick up upstream changes. Nothing is removed either: a template a release renamed or moved stays in the root beside its new copy, and the server stops at startup on the duplicate id, so delete the stale file. A shipped file you deleted comes back on the next run; empty it instead to keep it out.

Base migration preparation copies the versioned SQL distributed with Moongate. It does not generate new SQL from entities, load plugins, create databases or apply migrations. Use the persistence tools for entity changes after initialization.

Running the command again without certificate options preserves existing configuration and files. Missing bundled migrations are added only when the existing catalog is compatible. An existing migration with the same sequence number but a different filename or checksum stops initialization before configuration or SQL files are written. The error identifies the conflicting file. Do not delete migration history to bypass it; reconcile the catalog or choose a new root.

Without --generate-admin-certificate, an existing config is never rewritten, including its migration directory setting. Check that setting yourself if it points outside this root. If you move the root, update the absolute path in a newly generated config accordingly.

Continue with Start a Moongate server: edit the generated configuration, create the PostgreSQL databases, apply the bundled SQL with mgctl migrate apply and start the server. For development, you can instead enable persistence.auto_generate_migrations after preparing the databases; see the entity tutorial.

For a release image:

Terminal window
docker volume create moongate-data
docker run --rm --entrypoint /app/mgctl \
--mount type=volume,source=moongate-data,target=/data \
docker.io/tgiachi/moongate:latest init /data

Images up to 0.11.0 have /app/mgboot instead: use --entrypoint /app/mgboot and pass only /data. Initialization exits after preparing the mounted root; it opens no TCP listener.

Publish the server and tool into the same directory:

Terminal window
dotnet publish src/Moongate.Server -c Release -r linux-x64 -o dist/moongate
dotnet publish src/Moongate.Ctl -c Release -r linux-x64 -o dist/moongate
./dist/moongate/mgctl init /absolute/path/to/moongate-data

Change the runtime identifier for your platform. The server also exposes the same offline operation for source development:

Terminal window
dotnet run --project src/Moongate.Server -- --initialize-root --root-directory /absolute/path/to/moongate-data

The same certificate options work with the server’s offline operation:

Terminal window
dotnet run --project src/Moongate.Server -- --initialize-root \
--root-directory /srv/moongate --generate-admin-certificate \
--admin-certificate-hosts "login.example.test,192.0.2.10"

The server rejects certificate options outside --initialize-root.

MOONGATE_SERVER_EXECUTABLE can override the adjacent server executable for local tooling tests. It must refer to a compatible executable from the same source version.

Made with ❤️ by Tom

Buy me a coffee