@mason-data/cli 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +147 -69
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog.js +258 -14
  4. package/dist/src/api/postgres/encode.js +3 -1
  5. package/dist/src/api/postgres/explain.js +3 -2
  6. package/dist/src/api/postgres/protocol.js +39 -23
  7. package/dist/src/api/postgres/server.js +1290 -223
  8. package/dist/src/api/postgres/translate.js +81 -9
  9. package/dist/src/api/request-log.js +18 -5
  10. package/dist/src/cdc/pg-wire.js +144 -38
  11. package/dist/src/cdc/poll.js +5 -5
  12. package/dist/src/cli/background.js +55 -8
  13. package/dist/src/cli/cancel.js +75 -0
  14. package/dist/src/cli/command.js +5 -3
  15. package/dist/src/cli/commands.js +843 -272
  16. package/dist/src/cli/control.js +59 -0
  17. package/dist/src/cli/demo.js +158 -0
  18. package/dist/src/cli/dev.js +458 -125
  19. package/dist/src/cli/doctor.js +25 -12
  20. package/dist/src/cli/guides.js +103 -31
  21. package/dist/src/cli/init.js +159 -58
  22. package/dist/src/cli/machine.js +18 -12
  23. package/dist/src/cli/main.js +5 -5
  24. package/dist/src/cli/mcp.js +735 -0
  25. package/dist/src/cli/metrics.js +79 -0
  26. package/dist/src/cli/pg-tls.js +13 -3
  27. package/dist/src/cli/pg-url.js +4 -0
  28. package/dist/src/cli/pin.js +13 -12
  29. package/dist/src/cli/preflight.js +24 -14
  30. package/dist/src/cli/project.js +48 -7
  31. package/dist/src/cli/readiness.js +38 -4
  32. package/dist/src/cli/registry.js +57 -35
  33. package/dist/src/cli/replication-checks.js +7 -5
  34. package/dist/src/cli/replication-mode.js +105 -26
  35. package/dist/src/cli/request-filter.js +134 -0
  36. package/dist/src/cli/roles.js +584 -0
  37. package/dist/src/cli/run-server.js +498 -133
  38. package/dist/src/cli/runs.js +1 -1
  39. package/dist/src/cli/security.js +10 -8
  40. package/dist/src/cli/serve.js +147 -40
  41. package/dist/src/cli/share-run.js +286 -0
  42. package/dist/src/cli/share.js +286 -0
  43. package/dist/src/cli/source-edit.js +168 -0
  44. package/dist/src/cli/sources.js +290 -39
  45. package/dist/src/cli/supervise.js +12 -1
  46. package/dist/src/cli/sync.js +55 -25
  47. package/dist/src/cli/tunnel.js +200 -0
  48. package/dist/src/cli/ui.js +9 -6
  49. package/dist/src/cli/usage.js +128 -0
  50. package/dist/src/cli/user.js +496 -40
  51. package/dist/src/cli/version.js +9 -7
  52. package/dist/src/demo/seed.js +240 -0
  53. package/dist/src/demo/source.js +655 -0
  54. package/dist/src/duck/cancel.js +10 -1
  55. package/dist/src/duck/distinct-key.js +228 -0
  56. package/dist/src/duck/events.js +134 -0
  57. package/dist/src/duck/executor-worker.js +25 -11
  58. package/dist/src/duck/executor.js +107 -53
  59. package/dist/src/duck/explain.js +158 -29
  60. package/dist/src/duck/extensions.js +160 -0
  61. package/dist/src/duck/functions.js +73 -19
  62. package/dist/src/duck/gaps.js +25 -11
  63. package/dist/src/duck/grants.js +176 -0
  64. package/dist/src/duck/history.js +123 -0
  65. package/dist/src/duck/jsonb-order.js +175 -0
  66. package/dist/src/duck/jsonb.js +10 -8
  67. package/dist/src/duck/kept-route.js +82 -2
  68. package/dist/src/duck/kept.js +211 -44
  69. package/dist/src/duck/pgtree.js +40 -0
  70. package/dist/src/duck/plan.js +120 -30
  71. package/dist/src/duck/publication.js +6 -6
  72. package/dist/src/duck/requests.js +265 -93
  73. package/dist/src/duck/rewrite.js +1193 -73
  74. package/dist/src/duck/row-filter.js +649 -0
  75. package/dist/src/duck/store.js +271 -159
  76. package/dist/src/duck/stream.js +10 -5
  77. package/dist/src/duck/sync.js +907 -275
  78. package/dist/src/duck/types.js +6 -5
  79. package/dist/src/duck/usage.js +324 -0
  80. package/dist/src/duck/users.js +64 -15
  81. package/dist/src/duck/verified.js +9 -0
  82. package/dist/src/duck/views.js +2 -3
  83. package/dist/src/lib/database-name.js +62 -0
  84. package/dist/src/lib/dotenv.js +17 -4
  85. package/dist/src/lib/log-buffer.js +68 -0
  86. package/dist/src/lib/pg-numeric.js +5 -2
  87. package/dist/src/lib/project-version.js +23 -23
  88. package/dist/src/lib/table-sync.js +267 -0
  89. package/dist/src/lib/timezone.js +3 -2
  90. package/dist/src/web/app.css +706 -55
  91. package/dist/src/web/app.js +330 -258
  92. package/dist/src/web/charts.js +229 -0
  93. package/dist/src/web/index.html +84 -15
  94. package/dist/src/web/pages/queries.js +192 -0
  95. package/dist/src/web/pages/status.js +446 -0
  96. package/dist/src/web/pages/syncs.js +155 -0
  97. package/dist/src/web/pages/tables.js +434 -0
  98. package/dist/src/web/pages/usage.js +397 -0
  99. package/dist/src/web/pages/users.js +218 -0
  100. package/dist/src/web/shared.js +568 -0
  101. package/dist/src/web/vendor/README.md +25 -0
  102. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  103. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  104. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  105. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  106. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  107. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  108. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  109. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  110. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  111. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  112. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  113. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  114. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  115. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  116. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  117. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  118. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  119. package/package.json +10 -3
package/README.md CHANGED
@@ -1,53 +1,66 @@
1
1
  # @mason-data/cli
2
2
 
3
- The `mason` command points [Mason](https://masondata.ai) at the Postgres your app already uses, copies the tables you
4
- add, keeps the copy current, and serves it to BI tools and your app over the Postgres protocol. Every command works the
3
+ The `mason` command points [Mason](https://github.com/embeddable-hq/mason-data) at the Postgres your app already uses, syncs the tables you
4
+ add, keeps them up to date, and serves them to BI tools and your app over the Postgres protocol. Every command works the
5
5
  same for you and for a coding agent (Claude Code, Codex, …): it prints for a person, or with `--json` as data, and its
6
6
  exit code says how it went ([for coding agents](#for-coding-agents)).
7
7
 
8
- It needs Node 22.13 or later and nothing else — no Docker, no database of its own; `mason doctor` checks this machine
9
- and says what is missing. On a terminal its output is coloured and wrapped to the window, and a command that takes a
10
- moment shows a spinner; piped, redirected, with `--json` or with `NO_COLOR` set, it is plain text.
8
+ There is nothing to clone or build: install this package and run `mason`. It needs Node 22.13 or later and nothing
9
+ else — no Docker, no database of its own, and nothing downloaded after `npm install`: Mason's postgres and inet
10
+ extensions come with it, as an optional dependency for your platform (`@mason-data/extensions-<platform>`, 17–31 MiB
11
+ to download; installing with `--omit=optional` leaves them out, and the first sync then downloads them).
12
+ `mason doctor` checks this machine and says what is missing. On a terminal
13
+ its output is coloured and wrapped to the window, and a command that takes a moment shows a spinner; piped,
14
+ redirected, with `--json` or with `NO_COLOR` set, it is plain text.
11
15
 
12
16
  **A Mason project is your app's folder.** `mason init` makes it one: its sources file in `mason/sources/` — the
13
- database and the tables to copy — yours to commit; this machine's copy of your data and Mason's state in `.mason/`,
17
+ database and the tables to sync — yours to commit; this machine's synced data and Mason's state in `.mason/`,
14
18
  both rebuilt from that file. Every other command works
15
- anywhere in the folder, as git does. `mason destroy` removes the copy and the state. ([Cleaning up](#cleaning-up))
19
+ anywhere in the folder, as git does. `mason destroy` removes the synced data and the state. ([Cleaning up](#cleaning-up))
16
20
 
17
21
  ```sh
18
22
  npm i -g @mason-data/cli
19
23
  cd my-app
20
24
  mason doctor # Node, memory, ports: what is missing, and what to do
21
- mason init # this folder becomes a project, on the database in .env's DATABASE_URL (checked first)
22
- mason start # Mason in the background, then returns
25
+ mason init # this folder becomes a project, on the database .env names (checked first)
23
26
  mason tables # the tables: rows, keys, added; a table's columns with `mason tables orders`
24
- mason add orders customers # copy them, added to the sources file (--columns id,status: those only)
25
- mason wait # until every added table is copied and served
27
+ mason add orders customers # add them to the sources file (--columns id,status: those only)
28
+ mason start # Mason in the background, syncing them, then returns
29
+ mason wait # until every added table is synced and served
26
30
  mason user add metabase # a user for a BI tool: its password, printed once, and the URL to paste
27
- mason ui # the status page and the users, in your browser
31
+ mason ui # the status, tables, users and statements, in your browser
28
32
  ```
29
33
 
30
34
  `mason init` takes the database's URL as an argument too (`mason init postgres://user:password@localhost:5432/your_app`);
31
- without one it reads `DATABASE_URL` from the folder's `.env.local`, `.env` or the shell. The sources file it writes says
32
- `url: ${DATABASE_URL}`, and the URL itself stays in `.env` (init writes the line there when you gave the URL as an
33
- argument or it was only in the shell, and gitignores that file): change it there when the password changes. The
34
- database is required.
35
+ without one it reads `MASON_DATABASE_URL`, else `DATABASE_URL`, each from the folder's `.env.local`, `.env` or the
36
+ shell — set `MASON_DATABASE_URL` to aim Mason at a database other than the one your app's `DATABASE_URL` names. The
37
+ sources file it writes names the variable it read, `url: ${DATABASE_URL}` or `url: ${MASON_DATABASE_URL}` (given a URL
38
+ as an argument, `MASON_DATABASE_URL` when your app's `DATABASE_URL` names another database), and the URL itself stays
39
+ in `.env` (init writes the line there when you gave the URL as an argument or it was only in the shell, and gitignores
40
+ that file): change it there when the password changes. The database is required.
41
+
42
+ **No database at hand?** `mason init demo && mason start`, in a new folder, makes a project on a demo database Mason
43
+ hosts itself — a small shop (`customers`, `products`, `orders`, `order_items`; the first three added, `order_items`
44
+ left for your first `mason add order_items` after `mason start`), kept in `.mason/demo/`, served on a port of this machine, with a new order every few seconds (`MASON_DEMO_CHANGE_MS`, default 5000) so
45
+ `mason status` shows the synced data keeping up. Nothing to install. It is read by polling only (it has no change log), and
46
+ `mason destroy` removes it with the synced data.
35
47
 
36
48
  ## The project: its sources file, in your app
37
49
 
38
50
  ```
39
51
  my-app/
40
- .env the database's URL (DATABASE_URL), as your app has it (gitignored)
41
- .mason/ this machine's: Mason's key, the copy of your data, its state and logs (gitignored)
42
- mason/sources/app.yml the database and the tables Mason copies — commit it
52
+ .env the database's URL (DATABASE_URL, as your app has it, or MASON_DATABASE_URL) (gitignored)
53
+ .mason/ this machine's: Mason's key, the synced data, its state and logs (gitignored)
54
+ mason/sources/app.yml the database and the tables Mason syncs — commit it
43
55
  mason/mason.yml the Mason version the project is written for — commit it: production runs that version
44
56
  ```
45
57
 
46
- The sources file is named for the database (`app.yml` above) and lists what Mason copies, one entry per table:
58
+ The sources file is named for the database (`app.yml` above) and lists what Mason syncs, one entry per table:
47
59
 
48
60
  ```yaml
49
61
  kind: postgres
50
62
  url: ${DATABASE_URL} # a variable from .env.local, .env or the shell; a literal URL is allowed for local dev
63
+ database: production # optional: the database name a tool connects with (none: mason)
51
64
  tables:
52
65
  orders: # a bare name is in `public`; `billing.invoices` names its schema
53
66
  columns: [id, customer_id, status, amount, created_at, payload]
@@ -55,12 +68,14 @@ tables:
55
68
  events:
56
69
  exclude: [raw_body] # every column but these
57
70
  sessions:
58
- key: [tenant_id, id] # only for a table with no primary key: the unique columns to copy it by
71
+ key: [tenant_id, id] # only for a table with no primary key: the unique columns to key it by
59
72
  ```
60
73
 
61
- - An entry is `{}` (every column), `columns:` (those; the primary key is always copied), `exclude:` (every column but
74
+ - An entry is `{}` (every column), `columns:` (those; the primary key is always synced), `exclude:` (every column but
62
75
  those) — never both — and, for a table with no primary key, `key:`: the columns that tell its rows apart, which it is
63
- copied by (polling only: logical replication cannot apply a change by them). A table with a primary key refuses `key:`.
76
+ keyed by (polling only: logical replication cannot apply a change by them). A table with a primary key refuses `key:`.
77
+ - A tool connects with the database name `mason`. A tool moving from your Postgres changes its host, port and database
78
+ name, or `database:` keeps the name it has, so it changes only its host and port.
64
79
  - Column types are not in the file: Mason reads them from the database at every start.
65
80
  - `url:` is `${NAME}`, read from `.env.local`, `.env` or the shell — or a literal URL, for a database on your machine.
66
81
  - One source per project for now, and `postgres` is its only kind.
@@ -70,11 +85,11 @@ tables:
70
85
  mistakes on one report, each with its key and the way out (`did you mean …?`).
71
86
 
72
87
  `mason init --folder <dir>` puts `sources/` elsewhere in the folder (`--folder .` for the folder itself). Keep it in
73
- your app's repo, so a schema change and the tables that copy it travel together. A teammate who clones the repo runs
88
+ your app's repo, so a schema change and the tables that sync it travel together. A teammate who clones the repo runs
74
89
  `mason init` once: the sources file is kept, and `.mason/` is per machine.
75
90
 
76
91
  **A project an older Mason made** (its tables listed in `mason/models/`) is refused by `mason start`; nothing is
77
- migrated. `mason init` in it writes the sources file alone — its `.mason/` and copy kept — then `mason add` each table
92
+ migrated. `mason init` in it writes the sources file alone — its `.mason/` and synced data kept — then `mason add` each table
78
93
  again and remove `mason/models/`.
79
94
 
80
95
  ## Commands
@@ -82,76 +97,88 @@ again and remove `mason/models/`.
82
97
  | Command | What it does |
83
98
  |---|---|
84
99
  | `mason help [<command> \| <guide>]` | The commands by step, one command's usage, or one of the guides, offline: `mason help agent` (driving Mason from a coding agent: the commands in order, `--json` and its shapes, exit codes, failures) and `mason help production` (the road from a laptop to production: the image, its settings, Docker Compose, logical replication, upgrades). |
85
- | `mason doctor` | Checks what Mason needs here — Node 22.13+, memory, Mason's ports — and in a project's folder its database; with Mason running for it, the run itself: whether every added table is copied, the statements refused or slow in the last day. Every failing line says what to do; it ends with the next command. |
86
- | `mason init [<postgres url>]` | Makes this folder a project: checks it can read the database (nothing is written when it cannot; `--skip-check` writes anyway), writes `.mason/config.json` (Mason's key, never the URL) and `mason/sources/<database>.yml` (keeping a sources file already there), adds the URL to `.env` when it was not there, and adds `.mason/` and that `.env` to `.gitignore`. The URL from the argument, else `DATABASE_URL` in `.env.local`, `.env` or the shell. `--name` sets the project's name (default: the folder's) — the database name a tool connects to — `--folder` where its files go, `--remote` allows a database on another machine. Refuses a folder already in a project, but for one with no sources file (an older Mason's), whose sources file alone it writes. |
87
- | `mason start` | Starts Mason for this folder's project in the background and returns once it is up. Run it again any time: it says so when Mason is already running. `--replication` reads the database's change log as production does (below); `--poll` goes back. `--self-signed-tls` serves the Postgres API over TLS; `--attach` stays in the foreground. |
100
+ | `mason doctor` | Checks what Mason needs here — Node 22.13+, memory, Mason's ports — and in a project's folder its database; with Mason running for it, the run itself: whether every added table is synced, the statements refused or slow in the last day. Every failing line says what to do; it ends with the next command. |
101
+ | `mason init [<postgres url>]` | Makes this folder a project: checks it can read the database (nothing is written when it cannot; `--skip-check` writes anyway), writes `.mason/config.json` (Mason's key, never the URL) and `mason/sources/<database>.yml` (keeping a sources file already there), adds the URL to `.env` when it was not there, and adds `.mason/` and that `.env` to `.gitignore`. The URL from the argument, else `MASON_DATABASE_URL`, else `DATABASE_URL`, each in `.env.local`, `.env` or the shell (the sources file names the one read). `--name` sets the project's name (default: the folder's), which its replication slot is named for; `--database` the database name a tool connects with (default `mason`, none written); `--folder` where its files go, `--remote` allows a database on another machine. Refuses a folder already in a project, but for one with no sources file (an older Mason's), whose sources file alone it writes. |
102
+ | `mason start` | Starts Mason for this folder's project in the background and returns once it is up. Run it again any time: it says so when Mason is already running. Each table is kept current as its `sync:` in the sources file says (`off`, `poll <interval>` or `replication`). `--self-signed-tls` serves the Postgres API over TLS; `--attach` stays in the foreground. |
88
103
  | `mason tables [<table>…] [--all]` | The tables this user can read against the sources file, one line each: rows, primary key, whether added (and with which columns), and why a table cannot be added yet (no primary key: the `--key` to add it by). Name a table (or pass `--all` for every one) for its columns: each one's type, what it references, and whether `mason add` would leave it out as a secret. It reads the project's database itself, so Mason need not be running. |
89
- | `mason add <table>… [--columns …] [--include …] [--key …]` | Copies tables into Mason, each one an entry of the sources file: every column (`orders: {}`), or `--columns id,status,created_at` for those (one table at a time; the key is written with them). Columns that look like secrets (`password`, `token`, `api_key`, `private_key`, …) are left out, named, and written as `exclude:`; `--include password_hash` adds one. `--key tenant_id,id` adds a table with no primary key, copied by those columns. Adding a table again with other columns changes its entry. |
90
- | `mason remove <table>…` | Takes tables out of the sources file — each one's entry, every other byte as it was — and Mason drops their copies. |
91
- | `mason wait [--timeout <s>]` | Waits until every added table is copied and served, showing every added table on a line of its own: 100% for the finished ones, in the order they finished, and a bar for each still copying (rows, %, rate, time left) at the bottom. Exit 0 ready, 1 a copy failed (printed, with what to do), 124 timed out (default 600 s). |
104
+ | `mason add <table>… [--columns …] [--include …] [--key …] [--sync …]` | Syncs tables into Mason, each one an entry of the sources file: every column (`orders: {}`), or `--columns id,status,created_at` for those (one table at a time; the key is written with them). Columns that look like secrets (`password`, `token`, `api_key`, `private_key`, …) are left out, named, and written as `exclude:`; `--include password_hash` adds one. `--key tenant_id,id` adds a table with no primary key, keyed by those columns. `--sync "poll 30s"` (or `off`) writes how it is kept current after its first sync on its entry; on a table already added, `--sync` alone changes that line and nothing else. Without it the entry follows the file's `sync:`, but a table over 1,000,000 rows under a file that polls is written `sync: off` and said, and a frequent poll of tens of millions of rows is warned of with an estimate of its cost. Adding a table again with other columns changes its entry. Mason need not be running: the entry is checked against the database as the run checks it, and the first sync begins at `mason start` (with Mason running, at once). |
105
+ | `mason remove <table>…` | Takes tables out of the sources file — each one's entry, every other byte as it was — and Mason drops their synced data: at once when it is running, else at the next `mason start`. Mason need not be running. |
106
+ | `mason wait [--timeout <s>]` | Waits until every added table is synced and served, showing every added table on a line of its own: 100% for the finished ones, in the order they finished, and a bar for each still syncing (rows, %, rate, time left) at the bottom. Exit 0 ready, 1 a first sync failed (printed, with what to do), 124 timed out (default 600 s). |
92
107
  | `mason user add\|list\|remove <name>` | The users of the Postgres API: a name and a password that does not expire, as a BI tool's datasource form asks for them or an app's `DATABASE_URL` carries them. `add` prints the password once with the URL; `remove` refuses the next connection at once and ends the sessions it holds at their next statement, without a restart. |
93
- | `mason ui` | Opens the running Mason's web page in your browser: what `mason status` says, refreshed every 2 s, and the users of `mason user` (add one, its password shown once; remove one). It prints the URL with the run's token, which the page trades for a cookie on the first visit; `--no-open` prints it only, `--json` is `{ url, port }`. `mason start`'s alone: `mason serve` serves no page. |
94
- | `mason status` | What Mason is doing: the database and how its changes are read, the project folder, the copy's file, the Postgres API and how it listens, each table's copy, the statements answered slowly in the last day and the ones refused or failed, each with its reason. In replication mode, where the stream is. |
95
- | `mason sync <table>…` | Makes the copy of these tables current now: a table over the sync's row limit (1,000,000 rows) is copied and served but kept current only so, or by replication. |
96
- | `mason logs [-n <lines>] [-f] [--start] [--requests [--failed]]` | Mason's log: every copy, poll and statement, with warnings and errors and their reasons; `-f` follows it, `--start` shows what `mason start` printed instead. `--requests` is the request log: every statement a tool sent, when, the outcome and SQLSTATE, how long, the rows, the tool and the user; `--failed` only the refused and failed ones. |
97
- | `mason stop` | Stops Mason for this project. The copy and the state stay: the next `mason start` resumes in seconds. |
98
- | `mason destroy` | Removes this project's copy of the data (its file) and Mason's state in `.mason/`, stopping Mason first if it runs — and, for a project started with `--replication`, its slot and publication on the database. Your files, `.mason/config.json` and the users stay: the next `mason start` copies the tables again. |
99
- | `mason serve` | Mason for a server or a container: `mason start --attach` configured by the environment alone, its state in one folder (a volume), its log on stdout, `GET /healthz`, a clean stop on SIGTERM. `mason serve --help` lists every setting with its default. |
108
+ | `mason mcp [--url <url>]` | An MCP server on stdio for Claude Code, Claude Desktop and other agents (below): the tables Mason serves with each one's rows and how current it is, a table's columns, a statement's rows with their Postgres types or Mason's refusal with its way out, and EXPLAIN — connected as a `mason user` (`MASON_URL`, else `--url`), beside `mason start` or a `mason serve` elsewhere. |
109
+ | `mason ui` | Opens the running Mason's web page in your browser: the Overview (what `mason status` says), the Tables (add, remove or sync one), the Users of `mason user` (add one, its password shown once; disable, enable or remove one) and Observability (the statements, usage over time, the sync's events), refreshed while the tab is open. It prints the URL with the run's token, which the page trades for a cookie on the first visit; `--no-open` prints it only, `--json` is `{ url, port }`. `mason start`'s alone: `mason serve` serves no page. |
110
+ | `mason status` | What Mason is doing: the database and how its changes are read, the project folder, the synced data's file, the Postgres API and how it listens, each synced table, the statements of the last day and week (each outcome, the completed ones' average, p50 and p95 time, the rows and bytes sent), the ones answered slowly in the last day and the ones refused or failed, each with its reason. In replication mode, where the stream is. |
111
+ | `mason sync <table>…` | Brings these synced tables up to date now, by one poll each: a table whose `sync:` is `off` is kept current only so (from cron, before a report), a polled one is polled now as well as at its interval. In replication mode the stream keeps every table current, and it says where the stream is. |
112
+ | `mason logs [-n <lines>] [-f] [--start] [--requests [<filters>]] [--events [--failed]] [--running]` | Mason's log: every sync, poll and statement, with warnings and errors and their reasons; `-f` follows it, `--start` shows what `mason start` printed instead. `--requests` is the request log: every statement a tool sent, when, the outcome (ok, refused, failed, cancelled) and SQLSTATE, how long, the rows and bytes sent, the tool and the user; `--failed` only the ones not answered, `--outcome`, `--since` / `--until` (`2h`, `7d`, an ISO time), `--user` and `--client` narrow it, and `--before` reads the page before. `--events` is the sync's events: each first sync, poll, catch-up or stream that failed, the database out of reach, a table current again, each sync asked by hand (mason sync, the page's Sync now), and each restart of the run (`--failed` only the failures). `--running` lists the statements running now, by session. |
113
+ | `mason cancel <session>` | Stops the statement a session of the Postgres API is running, as the client's own cancel would: the client is told 57014, its session goes on, and the request log keeps it as cancelled. `mason logs --running` lists the sessions. |
114
+ | `mason usage [--since 7d] [--step 1h]` | What Mason did over time, from a sample a minute kept `MASON_HISTORY_DAYS` days: the statements and their p95, the rows and bytes sent, the synced data's size and rows, the stalest table, the stream's lag, the memory; then each synced table's own size. |
115
+ | `mason stop` | Stops Mason for this project. The synced data and the state stay: the next `mason start` resumes in seconds. |
116
+ | `mason destroy` | Removes this project's synced data (its file) and Mason's state in `.mason/`, stopping Mason first if it runs — and, when a table was replicated, its slot (and its publication, when Mason made it) on the database. Your files, `.mason/config.json` and the users stay: the next `mason start` syncs the tables again. |
117
+ | `mason serve` | Mason for a server or a container: `mason start --attach` configured by the environment alone, its state in one folder (a volume), its log on stdout, `GET /healthz`, `GET /metrics` (Prometheus, numbers only), a clean stop on SIGTERM. `mason serve --help` lists every setting with its default. |
100
118
 
101
119
  Every command documents itself: `mason --help` lists them, and `mason <command> --help` (or `mason help <command>`) says
102
- how to use one, with examples. Every command but `help` takes `--json`. A command means the project of the folder it
120
+ how to use one, with examples. Every command but `help` and `mcp` (whose stdout is the MCP protocol's) takes `--json`. A command means the project of the folder it
103
121
  runs in, or the nearest one above it.
104
122
 
105
123
  ## What `mason start` does
106
124
 
107
- 1. **It only reads the database** — the one the sources file names. Mason polls it with `SELECT` statements. It does not
108
- need `wal_level = logical`, a replication slot, a publication or the `REPLICATION` attribute, and it creates none of
109
- them. When you stop it, nothing is left behind on the database. A table over 1,000,000 rows is copied and served but
110
- kept current only by `mason sync` or by replication, and `mason status` says so on its line. (`--replication` is the
111
- exception, said below: it reads the change log, as production does, and makes its own slot.)
112
- 2. **It copies each added table** into a DuckDB file in `.mason/`, a range of pages at a time, a few in parallel, one
113
- Postgres backend each, smallest table first; a copy that stops continues with the ranges it did not commit. The table
114
- is served the moment its copy lands; the copy runs beside the polls, never instead of them.
125
+ 1. **It only reads the database** — the one the sources file names, with `SELECT` statements. It does not need
126
+ `wal_level = logical`, a replication slot, a publication or the `REPLICATION` attribute, and it creates none of
127
+ them. When you stop it, nothing is left behind on the database. After its first sync each table is kept current as
128
+ the sources file's `sync:` says — at the top for every table, on an entry for that one: `off` (the default, and what
129
+ `mason init` writes) reads it again only at `mason sync <table>`; `poll 30s` polls it every 30 seconds, each poll a
130
+ scan of the table, the next one at least four times the last one's time later. Changing a table's `sync:` keeps its
131
+ synced data and catches it up by one poll. `mason status` says each table's sync and when it was last synced, on its line.
132
+ (`sync: replication` is the exception: that table is read from the change log, as production does, through one slot
133
+ Mason makes and a publication carrying the replicated tables alone — its own, or the one `publication:` at the top
134
+ of the file names, never altered. The slot is dropped, verified gone, once no table is replicated.)
135
+ 2. **It syncs each added table** into a DuckDB file in `.mason/`, a range of pages at a time, a few in parallel, one
136
+ Postgres backend each, smallest table first; a first sync that stops continues with the ranges it did not commit. The table
137
+ is served the moment its first sync lands; it runs beside the polls, never instead of them.
115
138
  3. **It serves the Postgres API** on 5433, or on the next port free on every address when something holds 5433 (often a
116
139
  second Postgres; `mason start` says what held it) — and records it: every later start listens on the same port, so a
117
140
  tool's saved connection keeps working, and refuses, naming what holds it, rather than move. `MASON_PG_PORT=55433` in
118
141
  your shell names the port, or moves the project to it (said). It listens on 127.0.0.1 unless `MASON_PG_HOST` names
119
142
  another address; off this machine it needs TLS (`--self-signed-tls`, or `MASON_PG_TLS_CERT` / `MASON_PG_TLS_KEY`).
120
- 4. **It runs in the background**, copying what the sources file names: `mason add` and `mason remove` change it at once,
121
- a hand edit at the next `mason start`.
143
+ 4. **It runs in the background**, syncing what the sources file names: `mason add` and `mason remove` change it at once,
144
+ a hand edit (or an add or remove made while Mason was stopped) at the next `mason start`.
122
145
 
123
- Mason's own settings (`MASON_POLL_MS`, `MASON_DUCKDB_MEMORY`, `MASON_COPY_PARALLEL`, …) set in your shell reach Mason.
124
- A copy reads `MASON_COPY_PARALLEL` chunks at once (default 3), each sized to take about `MASON_COPY_CHUNK_SECONDS` to
146
+ Mason's own settings (`MASON_DUCKDB_MEMORY`, `MASON_COPY_PARALLEL`, …) set in your shell reach Mason. How often a
147
+ table is polled is its `sync:`, never a setting: `MASON_POLL_MS` set refuses the start, naming the line to write.
148
+ A first sync reads `MASON_COPY_PARALLEL` chunks at once (default 3), each sized to take about `MASON_COPY_CHUNK_SECONDS` to
125
149
  read (default 12) at the rate it measures on your connection, so `mason wait` moves every few seconds even over a slow
126
- link, and a copy that stops reads at most those chunks again.
127
- A JSON value your queries keep reading (`payload ->> 'customer_id'`) is stored as a column of the copy and read from
150
+ link, and a first sync that stops reads at most those chunks again.
151
+ A JSON value your queries keep reading (`payload ->> 'customer_id'`) is stored as a column of the synced table and read from
128
152
  there (`mason status` lists them); `MASON_STORE_JSON_PATHS=off` turns that off.
129
153
 
130
154
  ## `mason start` options
131
155
 
132
156
  | Option | What it does |
133
157
  |---|---|
134
- | `--replication` | Read the database's changes from its change log (logical replication), as production does, instead of polling. It needs `wal_level = logical` and a user that may replicate, and makes its own publication and slot (`mason_dev_<project>_<source>`, the source the sources file's name; past 63 bytes, a prefix and a hash) on the database. The project keeps the mode until `--poll`; a switch keeps the copies and catches each table up by one poll. `mason stop` leaves the slot (it holds WAL until the next start); `mason destroy` and `--poll` drop it. |
135
- | `--publication <name>` | With `--replication`: a publication the database owner made (else `PG_PUBLICATION`), read as it is, verified to carry every added table and column, never created, altered or dropped. A read replica needs one. |
136
- | `--poll` | Read them with `SELECT` again (the default). |
137
158
  | `--self-signed-tls` | The Postgres API requires TLS, from a certificate made for this project in `.mason/` (served again by every later start with the flag); clients connect with `sslmode=require`. |
138
159
  | `--port <n>` | The port the `mason` commands reach it on (default 4039, or the next free one). |
139
160
  | `--attach` | Stay in the foreground: the terminal shows Mason's progress, and Ctrl-C stops it. |
140
161
 
141
- A database on another machine needs `mason init --remote`, which records that you mean it: the copy leaves the
142
- database's network, and each added table under the row limit is read whole every few seconds (`--replication` reads only
143
- its changes). Point it at a read replica, never the primary your app writes to.
162
+ `--replication`, `--poll` and `--publication` are gone: each answers with the line to write at the top of the sources
163
+ file (`sync: replication`, `sync: poll 5m`, `publication: <name>`) and exit 2, and starts nothing. A project that
164
+ streams, with no `sync:` anywhere in its file, is refused with its slot kept: `sync: replication` keeps streaming,
165
+ `sync: off` or `sync: poll <interval>` confirms the drop. One with a recorded publication needs `publication: <name>`,
166
+ or `publication: null` for Mason's own.
144
167
 
145
- **When the database moves.** Mason records the Postgres server it copied from. The same database on a new server (a
146
- restore, a move to another host) is read again at the next `mason start`: every row is compared with the copy, and only
147
- what changed is written. A different database is refused: `mason destroy`, then `mason start`, copies it.
168
+ A database on another machine needs `mason init --remote`, which records that you mean it: the synced data leaves the
169
+ database's network, and each polled table is read whole at its `sync:` interval (`sync: replication` reads only its
170
+ changes). Point it at a read replica, never the primary your app writes to.
171
+
172
+ **When the database moves.** Mason records the Postgres server it synced from. The same database on a new server (a
173
+ restore, a move to another host) is read again at the next `mason start`: every row is compared with the synced data, and only
174
+ what changed is written. A different database is refused: `mason destroy`, then `mason start`, syncs it.
148
175
 
149
176
  ## For coding agents
150
177
 
151
178
  `mason help agent` is the page an agent reads first, offline: the commands in the order a project takes them, every
152
179
  answer's shape, the exit codes and the failure codes. In short:
153
180
 
154
- - Every command but `help` takes `--json` and answers with **one JSON object on stdout**, always with
181
+ - Every command but `help` and `mcp` takes `--json` and answers with **one JSON object on stdout**, always with
155
182
  `next: [{ command, what }]` — the commands to run next, the same a person reads under `Next:`. The other keys are the
156
183
  command's own; `mason help <command>` names them on its `--json` line. Lists are wrapped (`{ tables: […], next }`).
157
184
  - A failure is `{ "error": { "code", "message", "why", "fix" }, "next": […] }` on stdout, and the **exit code** says it:
@@ -169,18 +196,67 @@ mason add orders customers --json && mason wait --json
169
196
  mason user add app --json # the password and URL for the app, as data
170
197
  ```
171
198
 
199
+ ### From an MCP client: `mason mcp`
200
+
201
+ A chat agent asks Mason with tools, over MCP, rather than through a shell. `mason mcp` is an MCP server on stdin and
202
+ stdout (newline-delimited JSON-RPC; stdout carries the protocol alone, what it says goes to stderr) that serves until
203
+ the client closes stdin; it takes no `--json`, and says a failure to start on stderr. It connects to the Postgres API as
204
+ a `mason user`, by the URL `mason user add` printed — `MASON_URL`, or `--url` — so it works beside `mason start` as against a `mason serve` elsewhere; TLS is as the URL's `sslmode`
205
+ says (a host off the machine: `require` when it names none). It never makes a user by itself: with no URL, it refuses
206
+ and says `mason user add claude`.
207
+
208
+ | Tool | Arguments | What it answers |
209
+ |---|---|---|
210
+ | `tables` | none | each table Mason serves, named as a statement writes it (`public.orders`, `public."Clients"`): `rows_about` (the synced table's estimated row count), `kept` (how it is kept current, as `mason status` says it) and `kept_current_by` (`polling`, `logical replication`, `copying again`, or null when nothing keeps it current now) |
211
+ | `describe` | `table` (as `tables` names it, or a bare name in `public`) | its columns with their Postgres types and NOT NULL, its primary key, its foreign keys, comments, and the same rows and `kept` |
212
+ | `query` | `sql`, `max_rows` (200, at most 10,000), `timeout_ms` (30,000, at most 300,000) | `columns` (name, Postgres type), `rows` as Postgres's text, `truncated` when `max_rows` cut it; a refusal is a tool error with the SQLSTATE, message, DETAIL and HINT (the way out) |
213
+ | `explain` | `sql`, `analyze`, `timeout_ms` | Mason's EXPLAIN: each synced table read with its rows and how current it is, the SQL it runs, the plan |
214
+
215
+ Its resources are `mason://help/agent` and `mason://help/mcp` (the two pages `mason help` prints). Every statement it
216
+ runs is in the request log under `application_name` `mason-mcp/<the client's name>` (`mason logs --requests`), a query
217
+ `max_rows` cut with the rows it sent; its catalog reads (the tables, a table's columns) are not, as no catalog read is.
218
+ A request the client cancels (`notifications/cancelled`) cancels its statement on the Postgres API.
219
+
220
+ Claude Code, in the project's folder with Mason running, the URL in the server's environment (the server's name
221
+ comes before `--env`, which takes every value after it):
222
+
223
+ ```sh
224
+ mason user add claude # the URL, printed once
225
+ claude mcp add mason --env MASON_URL="postgres://claude:<password>@127.0.0.1:5433/mason" -- mason mcp
226
+ # or on its command line, where ps shows it to this machine's users and claude mcp add prints it
227
+ claude mcp add mason -- mason mcp --url "postgres://claude:<password>@127.0.0.1:5433/mason"
228
+ ```
229
+
230
+ Claude Desktop, in `claude_desktop_config.json`. The app often starts a server without your shell's `PATH`, where
231
+ neither `mason` nor the `node` its first line asks for is found: name both by their absolute paths (`which node`,
232
+ `which mason`):
233
+
234
+ ```json
235
+ {
236
+ "mcpServers": {
237
+ "mason": {
238
+ "command": "/absolute/path/to/node",
239
+ "args": ["/absolute/path/to/mason", "mcp"],
240
+ "env": { "MASON_URL": "postgres://claude:<password>@127.0.0.1:5433/mason" }
241
+ }
242
+ }
243
+ }
244
+ ```
245
+
172
246
  ## Cleaning up
173
247
 
174
- Three things exist once a project has run: your **files** (the sources file, in your app), the **copy of your data** (a
248
+ Three things exist once a project has run: your **files** (the sources file, in your app), the **synced data** (a
175
249
  file in `.mason/`) and **Mason's state** (`.mason/`: its settings, logs and the request log). Each way of cleaning up
176
250
  keeps a different set:
177
251
 
178
252
  | You want | Run | Keeps |
179
253
  |---|---|---|
180
254
  | Pause | `mason stop` | the data, the state, your files: the next `mason start` resumes in seconds |
181
- | Copy the data again, or free its disk | `mason destroy` | your files, `.mason/config.json` and the users: the next `mason start` copies everything again |
255
+ | Sync the data again, or free its disk | `mason destroy` | your files, `.mason/config.json` and the users: the next `mason start` syncs everything again |
182
256
 
183
- Restarting the machine is a `mason stop` that nobody ran: the next `mason start` resumes.
257
+ Restarting the machine is a `mason stop` that nobody ran: the next `mason start` resumes. In a demo project
258
+ (`mason init demo`) the demo database is state too: `mason destroy` removes it with the synced data, and the next `mason start`
259
+ seeds it again.
184
260
 
185
261
  ## Requirements
186
262
 
@@ -188,7 +264,9 @@ Restarting the machine is a `mason stop` that nobody ran: the next `mason start`
188
264
 
189
265
  - Node 22.13 or later. The SQL parser (`libpg-query`), the catalog (`PGlite`) and the engine (DuckDB) ship as
190
266
  WebAssembly or prebuilt binaries: no native build, no Python or compiler needed.
191
- - Memory: the copy uses up to half the machine's by default (`MASON_DUCKDB_MEMORY=4GiB` sets it).
267
+ - Memory: the engine may use half the container's memory limit by default, else half the machine's RAM
268
+ (`MASON_DUCKDB_MEMORY=4GB` sets it; `mason doctor` says it). Sizing a server:
269
+ [deploy/mason-data/README.md#memory](https://github.com/embeddable-hq/mason-data/blob/main/deploy/mason-data/README.md#memory).
192
270
  - Postgres 13 or later. Each table needs a primary key, or a `key:` in the sources file (polling only).
193
271
 
194
272
  ## Security
@@ -197,14 +275,14 @@ Restarting the machine is a `mason stop` that nobody ran: the next `mason start`
197
275
  project's `.mason/config.json`, which a browser cannot send — or, for its web page (`mason ui`), the cookie the run's token is traded for on the first visit (HttpOnly, SameSite=Strict, its Host and every change's Origin checked). The Postgres API takes a user's password (`mason user`): a
198
276
  MAC over its name keyed by the project's API key, so rotating the key removes every user at once. Off loopback the Postgres API is TLS or refused.
199
277
 
200
- What a project keeps on this machine (the database URL, the API key, the run's token, the copy, its log and request
278
+ What a project keeps on this machine (the database URL, the API key, the run's token, the synced data, its log and request
201
279
  log) is in its `.mason/`, readable by your user only and gitignored by `mason init`.
202
280
 
203
281
  ## In production
204
282
 
205
283
  Only the project folder travels — `mason/`, with `mason.yml` naming the Mason version it is written for. Production runs
206
284
  it as one container, `ghcr.io/embeddable-hq/mason-data`, with `mason serve`: the same run, configured by the environment,
207
- its copy on one volume, reading Postgres's change log (`MASON_CHANGES=replication`) or polling it. `mason help
285
+ its synced data on one volume, each table read from Postgres's change log or polled as its `sync:` says. `mason help
208
286
  production` is the road, offline; `deploy/mason-data/` in the repo is the Compose file and its walkthrough.
209
287
 
210
288
  Apache-2.0.
@@ -16,3 +16,6 @@ export class ApiError extends Error {
16
16
  this.code = code;
17
17
  }
18
18
  }
19
+ /** The code of a DISABLED `mason user`'s refused login or session (src/duck/users.ts): the listener answers it 28000, as
20
+ * Postgres answers a role without LOGIN, never 28P01 — its password was right. */
21
+ export const USER_DISABLED = "user_disabled";