The tmc CLI - The Modding Community

Not Released Yet

tmc is a command-line client for our public content API. It creates, edits and deletes your assets, mods, servers, articles, communities, collections and groups — and, because that is what the API is actually used for, it uploads files and cuts releases in one command.

Python 3.10+Zero dependenciesMIT licensed

tmc is not released yet. It is not on PyPI and has no public repository — today it installs from a source checkout, and this page describes what the tool already does rather than announcing a launch.

install.sh
# From the source checkout — there is nothing to build
pip install .
pip install '.[fast]'   # optional: cryptography, for faster signing
tmc --version       # tmc 1.1.0

# Keys are made under Account → API Keys
tmc auth login --token tmc_xxxxxxxxxxxx
tmc auth whoami

# …or read public items with no key at all
tmc mod list --anon --app 4 -o json

A single standard-library package, so there is nothing to build and nothing to resolve — and every command, flag and exit code is written up in the docs.

Releases

The Reason To Have A Tool At All

Cutting a release through the raw API is four requests in a particular order: upload the files, collect their ids, read the release set that already exists, then write the new one back without disturbing the others. release publish is that sequence.

publish.sh
# Upload the files, attach them, write the release — one command
tmc release publish --mod 5 \
  --version 1.2.0 \
  --title "Bug fixes" \
  --content-file CHANGELOG.md \
  --file 'dist/*.zip' --file dist/checksums.txt

# Everything on the item, without re-sending the item
tmc release list  --mod 5
tmc release files --mod 5 --version 1.2.0

Re-Running It Updates

The same --version twice updates that release and merges the new files into its set. Your other releases are never touched, and a hidden release stays hidden.

Globs, Batched

Pass --file 'dist/*.zip' and as many files as you have; the twenty-per-request upload cap is handled for you.

Big Files Stream

A 1 GB upload is read from disk in chunks, not into memory. Anything over your key's size limit is flagged before it is sent.
What It Does

Everything The API Does, Spelled Out

One command per thing you would do in the browser, and a few you would not want to — bulk edits, pipelines and scripted releases.

No Dependencies At All

Python 3.10+ and the standard library. Drop it on a build machine or a game server with no package index and it runs. cryptography is used for signing if it happens to be installed and a bundled RFC 8032 implementation if it is not — same signatures either way.

Both Auth Modes

Bearer tmc_ keys, or Ed25519 signed assertions where you hold the private key and we only ever store the public half.

One Grammar For Every Type

The same five verbs — list, get, create, update, delete — on all fourteen types, twelve of which get a command of their own. tmc content <type> is the uniform form, and the only way to the free-standing release and media types.

Relations Without Re-Sending

Tags, media, releases, links, sources and collection items are managed on their own, so adding one screenshot does not mean rewriting the whole item.

Bulk, Within Every Cap

Create or edit from a JSON file of any length. Every server cap — 25 per write, 100 per delete, 200 relation members, 500 keys on a relation delete, 20 upload parts — is batched for you, and a partial failure names the element that failed so you can resume rather than start over.

Typos Caught Locally

Field names are checked against a local mirror of the API's schemas, with the near-miss: 'mod' has no field 'tgs'. Did you mean 'tags'? tmc schema mod prints the field list it is checking against.

Built For Pipes

Seven output formats — table, json, jsonl, csv, tsv, yaml, ids — with --field to keep only the columns you want. Data on stdout, progress on stderr, and tmc completion bash|zsh|fish for the shell you are in.

Rate Limits Handled

A 429 is waited out for exactly as long as the API asks, up to --retry-wait-max, then tells you how long is left rather than hanging a build. Backoff applies only to idempotent methods — a POST is never silently repeated.
Credentials

Keys, Profiles And The Public Half

Log in once and the credential is stored in ~/.config/tmc/config.json at mode 0600 — or hand it over in the environment and nothing touches disk at all. One profile per site or per key, and --anon for the half of the API that needs no key in the first place.

auth.sh
# A bearer key, or an Ed25519 key you hold the private half of
tmc auth login --token tmc_xxxxxxxxxxxx
tmc auth login --jwt --key-id tmcak_xxxxxxxx --private-key ~/keys/tmc.pem

# A second key on the same site — a scoped one for scripts
tmc auth login --profile ci \
  --base-url https://moddingcommunity.com --token tmc_…
tmc auth use ci

# Which key is active, and what the server lets it do
tmc auth whoami
tmc auth doctor

# …or no key at all
tmc mod list --anon --app 4 -o json

A Profile Per Site Or Key

--profile names one, tmc auth use switches the default, and every stored value has an environment twin — TMC_TOKEN, TMC_KEY_ID, TMC_PRIVATE_KEY_FILE, TMC_BASE_URL, TMC_PROFILE.

Ask What A Key Can Actually Do

auth whoami establishes the key's real permissions with three deliberately harmless probes — a read, a create that cannot validate, and a delete of an empty id list. Nothing is created or deleted; --read-only sends only the read.

A Doctor For The Setup

auth doctor reports which signing backend is in use, whether your config file's permissions are safe and whether the site answers at all — the three things worth knowing before debugging a pipeline.

Reading With No Key At All

--anon reads the public summary of seven types with no Authorization header — a genuinely different endpoint, so it refuses writes before the request leaves, drops the filters that surface does not have rather than letting them look like they matched, and never quietly upgrades itself to a stored key. --set apiPublic=false is how an item leaves it.
Relations

Four Verbs That Say What They Do

The dangerous one is set: on this API a PUT to a relation means "this is now the complete set", so a one-member PUT deletes everything else on the item. The CLI keeps the four apart and warns before it sends the destructive one.

Command Method Means
tmc rel add POST Merge these in, leave everything else
tmc rel set PUT This is now the complete set
tmc rel rm DELETE Drop the ones you named
tmc rel clear DELETE Drop all of them
relations.sh
# Add two tags. The rest of the item is untouched
tmc tags add mod 5 pvp vanilla

# Upload a screenshot and attach it in one step
tmc media add mod 5 --file shot.png --title Screenshot

# Read one relation back
tmc rel get mod 5 releases

# …and the one that REPLACES the whole gallery
tmc rel set mod 5 media --from-file gallery.json
Automation

Made To Live In CI

Build your project, cut a release and upload its files on every tag. Credentials come from the environment, so nothing is written to disk on a runner.

release.yml
- name: Publish to TMC
  env:
    TMC_TOKEN: ${{ secrets.TMC_TOKEN }}
  run: |
    tmc release publish --mod 42 \
      --version "${GITHUB_REF_NAME#v}" \
      --content-file CHANGELOG.md \
      --file 'dist/*.zip'

# Clean pipes: data on stdout, progress on stderr
tmc mod list --mine --all -o ids | xargs -n1 tmc mod get -o json

Nothing Written To A Runner

The credential comes out of the environment, so no profile is created and nothing outlives the job. A JWT key can stay a secret file the runner mounts — we only ever hold its public half.

Exit Codes That Mean Something

2 usage, 3 auth, 4 not found, 5 validation, 6 rate limited, 7 server, 8 network — so a pipeline can tell "retry this" from "fix your key".

Never Blocked By A Stale Build

A field the tool has not learned about yet still goes through with --allow-unknown-fields, and tmc raw sends any request at all.

Tested Against The Real Thing

75 tests drive the actual CLI against an in-process mock of the API over a real socket — both auth modes, the batching, the relation semantics and the retry path.

Keys Live Under Account → API Keys

A key is free, takes a moment to make, and carries read / write / delete scopes, expiry, IP allow-lists and its own rate limit — so a key you hand to a build server can do exactly one thing. Every command, flag and exit code is written up in the docs.

A key is useful today — the API is live and the docs are written. tmc itself is still unreleased: no PyPI package and no public repository yet, so it is a source checkout until that changes.