A modern MUSH-like server in TypeScript/Deno. Full TinyMUX 2.x softcode,
versioned REST API, public portal (@ursamu/site) and staff console
(@ursamu/web), and zero external database dependencies — TypeGraph with
PGlite (local/in-memory PostgreSQL) by default, Deno KV as a fallback.
- Quick Start
- Features
- Architecture
- Configuration
- Tasks
- Plugins
- Plugin Development
- REST API
- Stdlib (v2.5.1+)
- Docker
- Production Deployment
- Testing
- Documentation
- Contributing
- License
Prerequisite: Deno 2.x.
# Scaffold a new game (engine + portal stack) from JSR
deno run -A jsr:@ursamu/cli@0.1.2/create my-game
cd my-game
# Start the supervised server
deno task startDefault listeners (scaffolded games):
| Port | Protocol | Purpose |
|---|---|---|
4201 |
Telnet | Legacy MU* clients |
4202 |
WebSocket | Game WS hub |
4203 |
HTTP | REST API + static portal |
Open the public site at http://localhost:4203/ (when @ursamu/site is
installed with plugins.site.serveRoot: true). Staff console:
http://localhost:4203/admin/. Play client: /play. Telnet:
telnet localhost 4201.
First staff account: with an empty database, register via the web
UI (/register or site login). The first web registrant receives
superuser. Telnet create <name> <password> still works for the
first player when no accounts exist.
git clone https://github.com/UrsaMU/ursamu.git
cd ursamu
# Prefer scaffolding a child game with --local for engine work:
deno run -A packages/cli/src/create.ts my-game --local
cd my-game && deno task startEngine
- TinyMUX 2.x softcode — full evaluator with ~250 stdlib functions
(math/string/list/logic/object/registers/output), action commands
(
@switch,@dolist,@if,@while,@break,@trigger,@wait),$-pattern command attrs and^-pattern listener attrs with%0–%9capture groups, master-room routing, zones (@zone). - Sandboxed scripting — every script runs in an isolated Web Worker
with the full
IUrsamuSDK. A bad script cannot crash the server. - Native command surface — 102 built-in commands (admin, building,
comms, channels, queries, status, auth) registered via
addCmd. - Zero external database dependencies — TypeGraph (built on Postgres/PGlite) running locally or in-memory is the default datastore, with a fallback Deno KV adapter available. No external database server setup required.
- Hot reload —
@reloadswaps commands, config, scripts, and plugins without disconnecting players.@rebootand@updatesendSIGUSR2to the supervised parent for no-disconnect restarts; the Telnet sidecar persists and JWT clients auto-reauth.
Plugin system (v2.6.0)
- Manifest-driven installs with semver-aware dependency resolution
(
deps[].versionsupports ranges like^1.2.0or>=1.0.0 <2.0.0). - Fail-fast atomic installs — a single failure anywhere in the manifest (unsafe name/URL, clone failure, version mismatch, conflict between requesters) aborts the run and rolls back every directory and registry mutation. Previously installed plugins are untouched.
- Typed error hierarchy:
PluginInstallErrorbase plusPluginDepNameError,PluginDepUrlError,PluginCloneError,PluginRenameError,PluginVersionError,PluginSemverError,PluginConflictError. - 10 official plugins ship pre-wired; community plugins drop into
plugins.manifest.json.
Formatting pipeline (v2.3.x – v2.4.x)
- 8 format slots:
NAMEFORMAT,DESCFORMAT,CONFORMAT,EXITFORMAT,WHOFORMAT,WHOROWFORMAT,PSFORMAT,PSROWFORMAT, plus arbitrary plugin-definedUPPERCASEslot names. registerFormatHandlerfor TS handlers;registerFormatTemplatefor raw MUSH-softcode templates (v2.4.0).resolveFormat/resolveGlobalFormatexposed onu.utilfor both TS and softcode contexts.
Locks (v2.2.0+)
- Boolean expressions with
&&,||,!, parentheses (legacy&/|still parsed). registerLockFuncfor custom lockfuncs alongside built-ins (flag,attr,type,is,holds,perm). Fail-closed parsing, 4096-char / 256-token caps.
Supervised game scaffold (v2.4.0+)
daemon.sh/stop.sh/restart.sh/status.shdriven bysrc/cli/start.ts. SIGUSR2 restart path keeps connections live.- Fresh
JWT_SECRETwritten to.envon scaffold; loaded via@std/dotenv.
Transport & auth
- WebSocket JWT pre-auth (
?token=<jwt>&client=web), per-socket rate limit (10 cmds/sec). - Telnet sidecar with byte-level IAC stripping (NAWS/option bytes no longer leak into commands).
- HTTP rate limit on
/auth/*endpoints. - MXP negotiation and
%mxp[cmd|text]substitutions.
Game systems
- GameClock — pluggable in-game calendar with
timeMultiplier. - Channels —
@chancreate/@chandestroy/@chanset, history, aliases, auto-join. - Pluggable stat systems via
registerStatSystem. - UI manifest (
/api/v1/ui-manifest) for web-client component registration.
| Package | JSR | Purpose |
|---|---|---|
@ursamu/core |
jsr:@ursamu/core |
Transport, DB (TypeGraph/PGlite or Deno KV), plugin lifecycle, events |
@ursamu/mush |
jsr:@ursamu/mush |
MUSH world — IDBObj, flags, locks, softcode, addCmd, IUrsamuSDK |
@ursamu/cli |
jsr:@ursamu/cli |
Project/plugin scaffold, update, package picker |
@ursamu/site |
jsr:@ursamu/site |
Public portal shell — nav, skins, /play client |
@ursamu/web |
jsr:@ursamu/web |
Staff console SPA at /admin/ |
@ursamu/ursamu |
jsr:@ursamu/ursamu |
Back-compat shim re-exporting @ursamu/mush |
New projects import from jsr:@ursamu/mush. Plugins still on
jsr:@ursamu/ursamu keep working.
packages/
├── core/ @ursamu/core
├── mush/ @ursamu/mush (engine)
├── cli/ @ursamu/cli
├── site/ @ursamu/site (public FE)
├── web/ @ursamu/web (staff FE)
├── help/ bbs/ mail/ wiki/ …
└── …
Scaffolded games pin @ursamu/mush and declare plugins under
config/config.json → server.plugins. Default portal stack:
builder, channels, help, bbs, mail, wiki, web, site.
| Surface | Contract |
|---|---|
Public site + /play |
packages/site/design.md |
Staff console /admin |
packages/web/design.md |
Both share the UrsaMU violet night token family
(tokens.css / staff-theme.css). Skins remap site tokens only.
Player input
→ WebSocket hub (rate limit)
→ cmdParser middleware stack:
1. Interceptors (object/room scripts)
2. SCRIPT_NODE bypass (attached scripts)
3. addCmd() registrations (plugin + native)
4. registerScript overrides (local → plugin → engine)
5. Exit matching / channel alias dispatch
→ Sandbox Worker (for scripts) receives `u` (IUrsamuSDK)
→ Result posted back, broadcast to room/socket
Everything lives in the database. Each plugin gets a namespaced DBO<T>
instance keyed by <plugin>.<collection>. By default, UrsaMU uses TypeGraphAdapter
(storing records inside a local/in-memory PGlite PostgreSQL database), with a fallback DenoKvAdapter.
const notes = new DBO<Note>("myplugin.notes");
await notes.create({ title: "Hello", body: "World" });
const all = await notes.query({});A scaffolded project writes config/config.json and .env for you.
For source checkouts, copy config.sample.json:
{
"server": {
"telnet": 4201,
"ws": 4202,
"http": 4203,
"db": "data/ursamu.db",
"corsOrigins": "*",
"maxConnectionsPerIp": 20
},
"game": {
"name": "My Game",
"description": "A UrsaMU-powered MUSH.",
"version": "0.0.1",
"playerStart": "1",
"timeMultiplier": 1
}
}| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET |
Recommended | random per restart | HMAC secret for JWT signing. Scaffolded projects write a fresh secret to .env. |
URSAMU_HTTP_PORT |
No | 4203 |
Override HTTP/WS hub port. |
URSAMU_TELNET_PORT |
No | 4201 |
Override Telnet sidecar port. |
.env is auto-loaded via @std/dotenv from src/main.ts.
Run with deno task <name>.
| Task | Description |
|---|---|
start |
First-run setup, then spawn game server + Telnet sidecar (supervised). |
dev |
Development mode with live logs. |
server |
Run game server only. |
telnet |
Run Telnet sidecar only. |
daemon |
Start both processes in background (managed restart loop). |
stop / restart / status |
Daemon control. |
logs |
Tail logs/main.log and logs/telnet.log. |
test |
Run the full test suite (1464+ tests). |
test:coverage |
Run tests with LCOV coverage. |
config |
Interactive configuration tool. |
create |
Scaffold a new game project or plugin. |
cli |
Interactive CLI menu. |
install-cli |
Install ursamu as a global command. |
docker:build / :up / :down / :logs |
Docker Compose lifecycle. |
CLI (from JSR):
deno run -A jsr:@ursamu/cli@0.1.2/create my-game
deno run -A jsr:@ursamu/cli@0.1.2/create plugin my-feature
deno run -A jsr:@ursamu/cli@0.1.2/ursamu # interactive menuSubcommands: create, plugin list|install|update|remove|search|info,
update, scripts list, package picker.
All plugins are declared in src/plugins/plugins.manifest.json and
installed on first startup via ensurePlugins — no manual steps.
| Plugin | Min Engine | Description |
|---|---|---|
| channel | >=1.9.27 |
Channel system — aliases, auto-join, history. |
| rhost-vision | >=1.8.0 |
Rhost-style look, who, score, +finger, +where, +staff. |
| discord | >=1.9.0 |
Webhook Discord bridge with reconnect/backoff. |
| jobs | >=1.9.0 |
Job/request tracking + REST API. |
| events | >=1.9.2 |
In-game event calendar with RSVP + REST API. |
| bbs | >=1.9.0 |
Bulletin boards — threading, IC/OOC tags, sticky posts. |
| wiki | >=1.9.0 |
Markdown wiki — pages, search, history, backlinks. |
>=1.9.3 |
In-game mail — drafts, folders, quota, REST API. | |
| builder | >=1.9.5 |
World-building (@dig, @open, @link, @describe, …). |
| help | >=1.9.0 |
API-first help — file/DB/command providers. |
{
"name": "my-plugin",
"url": "https://github.com/example/my-plugin",
"ref": "v1.0.0",
"description": "What this plugin does.",
"ursamu": ">=2.6.0",
"deps": [
{ "name": "jobs", "url": "https://github.com/UrsaMU/jobs-plugin", "version": "^1.9.0" },
{ "name": "channel", "url": "https://github.com/UrsaMU/channel-plugin" }
]
}Each deps[] entry may include an optional version semver range
(e.g. ^1.2.0, >=1.0.0 <2.0.0). When present, the installer reads
the dependency's own ursamu.plugin.json version and verifies the
range. Entries without version install unconditionally — older
manifests keep working.
Atomic installs (v2.6.0). ensurePlugins is fail-fast across the
entire manifest. If any plugin or transitive dep fails to clone, has an
unsafe name or URL, declares a version that violates a requested range,
or has conflicting ranges from multiple requesters, the entire run
aborts and rolls back. Nothing from the failed run is left on disk or
in src/plugins/.registry.json.
Scaffold a plugin inside any project:
deno run -A jsr:@ursamu/cli@0.1.2/create plugin my-featureGenerated layout:
src/plugins/my-feature/
├── index.ts IPlugin lifecycle — init() and remove()
├── commands.ts addCmd() registrations
├── router.ts registerPluginRoute() REST endpoints
└── db.ts DBO<T> namespaced storage
Minimal plugin:
import type { IPlugin } from "jsr:@ursamu/mush";
import { addCmd, gameHooks } from "jsr:@ursamu/mush";
// Note: jsr:@ursamu/ursamu is a backwards-compat shim and still works.
const onLogin = (e: { id: string }) => { /* … */ };
export default {
name: "my-feature",
version: "1.0.0",
description: "Example plugin.",
init() {
addCmd({
name: "greet",
pattern: /^greet\s+(.+)/i,
category: "Social",
help: "greet <name> — Wave at someone.",
exec: async (u) => {
const name = u.util.stripSubs(u.cmd.args[0] ?? "").trim();
u.send(`You wave at ${name}.`);
u.broadcast(`${u.util.displayName(u.me, u.me)} waves at ${name}.`);
},
});
gameHooks.on("player:login", onLogin);
return true;
},
remove() {
gameHooks.off("player:login", onLogin);
},
} satisfies IPlugin;See docs/plugins/ for the full guide including REST routes, DB access, hooks, format handlers, lockfuncs, and publishing.
The engine exposes a versioned REST API at /api/v1/.
| Endpoint | Auth | Description |
|---|---|---|
POST /api/v1/auth/login |
public | Authenticate, receive a JWT. |
POST /api/v1/auth/connect |
public | Create + connect (scaffold). |
POST /api/v1/auth/logout |
token | Invalidate session. |
POST /api/v1/auth/reset-password |
public | Request a reset email. |
POST /api/v1/auth/reset/:token |
public | Submit new password. |
GET /api/v1/me |
token | Current player. |
GET /api/v1/players/online |
token | Connected players. |
GET /api/v1/channels |
token | List channels. |
GET /api/v1/channels/:id/history |
token | Channel history. |
GET /api/v1/dbos |
token | List DBO collections. |
GET|PUT|DELETE /api/v1/dbobj/:id |
token | Object CRUD. |
POST|GET|PUT|DELETE /api/v1/dbobj/:id/attrs[/:name] |
token | Attribute CRUD. |
GET|POST /api/v1/scenes |
token | List / create scenes. |
GET|PUT|DELETE /api/v1/scenes/:id |
token | Scene CRUD. |
POST|PUT|DELETE /api/v1/scenes/:id/pose[/:poseId] |
token | Pose CRUD. |
GET /api/v1/scenes/:id/export?format=markdown|json |
token | Export scene. |
GET /api/v1/scenes/locations |
token | Locations with scenes. |
GET /api/v1/config |
public | Public config. |
GET /api/v1/ui-manifest |
token | Registered UI components. |
Plugin routes attach via registerPluginRoute. Full reference:
docs/api/rest.md.
Math/spatial/noise/physics primitives are re-exported from mod.ts
for direct use in TypeScript plugins — no npm:simplex-noise or
npm:alea needed.
import {
Rng, Noise,
lerp, smoothstep, clamp, remap,
dist2d, dist3d, angle2d, bearing,
vreflect, pointInAabb, rayAabb,
fbm2, ridged2, perlin2, simplex2, worley2,
} from "jsr:@ursamu/mush";
const rng = new Rng(12345); // per-instance seedable mulberry32
const noise = new Noise(rng.next()); // per-instance Perlin/Simplex/Worley
const h = fbm2(noise, x, y, 6, 2.0, 0.5);Full surface listed in docs/api/core.md.
deno task docker:build
deno task docker:up
deno task docker:logs
deno task docker:downThe Compose stack mounts three volumes:
volumes:
- ./data:/app/data # Database storage directory (TypeGraph/PGlite or Deno KV)
- ./config:/app/config # Game configuration
- ./logs:/app/logs # Server logsSet JWT_SECRET in .env before starting.
deno task daemon # start
deno task status # check
deno task logs # tail
deno task restart # SIGUSR2 — no-disconnect restart
deno task stop # stopThe supervisor restarts the main process on exit code 75 (used by
@reboot and @update); exit 0 (@shutdown) stops cleanly. The
Telnet sidecar persists across restart/@reboot; JWT clients
auto-reauth.
server {
listen 443 ssl;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:4203;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}Point Telnet clients directly at port 4201.
@update pull origin main, SIGUSR2 restart
@update main pull specific branch
@reboot SIGUSR2 restart (no git pull)
@shutdown clean stop
deno task test
deno task test:coveragePre-commit checklist (mirrors CI):
deno check --unstable-kv mod.ts
deno lint
deno test tests/ --allow-all --unstable-kv --no-check
deno test tests/security_*.test.ts --allow-all --unstable-kv --no-check1464 tests, 0 failures; 480 files lint-clean.
| Topic | Link |
|---|---|
| Installation | docs/guides/installation.md |
| CLI reference | docs/guides/cli.md |
| User guide | docs/guides/user-guide.md |
| Admin guide | docs/guides/admin-guide.md |
| Deployment | docs/guides/deployment.md |
| Plugin development | docs/plugins/index.md |
| REST API | docs/api/rest.md |
| Public site design | packages/site/design.md |
| Staff console design | packages/web/design.md |
| Architecture | docs/about.md |
Pull requests are welcome. For major changes please open an issue first to discuss your plan.
git clone https://github.com/YOUR-USERNAME/ursamu.git
cd ursamu
deno task test
deno run -A packages/cli/src/create.ts plugin my-test-featureSee docs/development/contributing.md for coding conventions and PR process.
MIT — see LICENSE.
