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.
TAB completion
Section titled “TAB completion”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:
eval "$(mgctl completion bash)" # bash: add the line to ~/.bashrcsource <(mgctl completion zsh) # zsh: add the line to ~/.zshrc, after compinitmgctl completion fish | source # fish: or save it as ~/.config/fish/completions/mgctl.fishThe 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.
Install a plugin package
Section titled “Install a plugin package”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.
mgctl plugin install Moongate.Plugins.OpenAi --root-directory /srv/moongatemgctl plugin install MyPlugin --repo myuser/my-plugins --root-directory /srv/moongatemgctl plugin install ./Moongate.Plugins.OpenAi.0.2.1.nupkg --root-directory /srv/moongatemgctl plugin install https://codeberg.org/moongate/moongate-plugins/releases/download/Moongate.Plugins.OpenAi-v0.2.1/Moongate.Plugins.OpenAi.0.2.1.nupkgmgctl plugin list --root-directory /srv/moongatemgctl plugin remove Moongate.Plugins.OpenAi --root-directory /srv/moongateinstall 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:
- Reads the nuspec: the package type must be
MoongatePluginand the id a plain folder name (letters, digits and.,-,_between them; no device names such asCON). - Checks the exact dependency on
Moongate.Server.CoreorMoongate.Hosting(a plugin may name either or both, at one version): it must be the version of thismgctl, 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. - Checks the bundle: at most 1000 files, no entry that leaves the bundle folder, and the
<id>.dlland<id>.deps.jsonthe loader needs. - 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].--yesskips the question. A plugin runs inside the server with all its permissions: install it only from a source you trust. - Unpacks only the
plugin/folder of the package, at most 100 MB, into a work folder insideplugins/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.
Install by id from a repository
Section titled “Install by id from a repository”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.
mgctl plugin install Moongate.Plugins.OpenAi # the community repositorymgctl plugin install MyPlugin --repo myuser/my-plugins # releases of a Codeberg repositorymgctl plugin install MyPlugin --repo https://codeberg.org/myuser/my-plugins # the same, as an addressmgctl plugin install MyPlugin --repo https://github.com/myuser/my-plugins # releases of a GitHub repositorymgctl plugin install MyPlugin --repo ./my-packages # a folder of .nupkg filesmgctl 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.--versionnames one version, a prerelease included, and is refused like a file when it was built for another Moongate. --repoand--versiongo 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(orGITHUB_TOKENfor 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_URLpointsmgctlat 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.
Run the server
Section titled “Run the server”mgctl run starts mgserver as a child process and keeps it up:
mgctl run --root-directory ~/moongatemgctl 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.
Prepare a server root
Section titled “Prepare a server root”mgctl init prepares a Moongate data directory without starting the server or connecting
to PostgreSQL. It is step 1 of the first-start sequence.
mgctl init /absolute/path/to/moongate-dataThe root directory is the single required positional argument. Relative paths are resolved from the current working directory. Quote paths containing spaces:
mgctl init "/srv/my realm"mgctl init --helpmgctl <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.
Generate an administration certificate
Section titled “Generate an administration certificate”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 = trueallow_insecure_loopback = falsecertificate_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.
What it creates
Section titled “What it creates”| 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.
Existing directories
Section titled “Existing directories”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.
Next steps
Section titled “Next steps”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.
Docker
Section titled “Docker”For a release image:
docker volume create moongate-datadocker run --rm --entrypoint /app/mgctl \ --mount type=volume,source=moongate-data,target=/data \ docker.io/tgiachi/moongate:latest init /dataImages 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.
Build from source
Section titled “Build from source”Publish the server and tool into the same directory:
dotnet publish src/Moongate.Server -c Release -r linux-x64 -o dist/moongatedotnet publish src/Moongate.Ctl -c Release -r linux-x64 -o dist/moongate./dist/moongate/mgctl init /absolute/path/to/moongate-dataChange the runtime identifier for your platform. The server also exposes the same offline operation for source development:
dotnet run --project src/Moongate.Server -- --initialize-root --root-directory /absolute/path/to/moongate-dataThe same certificate options work with the server’s offline operation:
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
