mandala-computer-mcp 0.3.0 → 0.5.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 (130) hide show
  1. package/README.md +462 -37
  2. package/dist/api.d.ts +12 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +262 -42
  5. package/dist/api.js.map +1 -1
  6. package/dist/artifacts.d.ts +63 -0
  7. package/dist/artifacts.d.ts.map +1 -0
  8. package/dist/artifacts.js +81 -0
  9. package/dist/artifacts.js.map +1 -0
  10. package/dist/cli.d.ts +4 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +35 -21
  13. package/dist/cli.js.map +1 -1
  14. package/dist/credentials.d.ts +33 -0
  15. package/dist/credentials.d.ts.map +1 -0
  16. package/dist/credentials.js +395 -0
  17. package/dist/credentials.js.map +1 -0
  18. package/dist/errors.d.ts +53 -10
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +122 -35
  21. package/dist/errors.js.map +1 -1
  22. package/dist/events.d.ts +11 -2
  23. package/dist/events.d.ts.map +1 -1
  24. package/dist/events.js +213 -95
  25. package/dist/events.js.map +1 -1
  26. package/dist/executions.d.ts +43 -0
  27. package/dist/executions.d.ts.map +1 -0
  28. package/dist/executions.js +162 -0
  29. package/dist/executions.js.map +1 -0
  30. package/dist/format.d.ts +11 -11
  31. package/dist/format.d.ts.map +1 -1
  32. package/dist/format.js +159 -16
  33. package/dist/format.js.map +1 -1
  34. package/dist/http.d.ts.map +1 -1
  35. package/dist/http.js +4 -0
  36. package/dist/http.js.map +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/limits.d.ts +17 -0
  42. package/dist/limits.d.ts.map +1 -0
  43. package/dist/limits.js +17 -0
  44. package/dist/limits.js.map +1 -0
  45. package/dist/paths.d.ts +42 -2
  46. package/dist/paths.d.ts.map +1 -1
  47. package/dist/paths.js +54 -3
  48. package/dist/paths.js.map +1 -1
  49. package/dist/poll.d.ts +5 -1
  50. package/dist/poll.d.ts.map +1 -1
  51. package/dist/poll.js +120 -16
  52. package/dist/poll.js.map +1 -1
  53. package/dist/results.d.ts +97 -0
  54. package/dist/results.d.ts.map +1 -0
  55. package/dist/results.js +263 -0
  56. package/dist/results.js.map +1 -0
  57. package/dist/server.d.ts +3 -2
  58. package/dist/server.d.ts.map +1 -1
  59. package/dist/server.js +61 -9
  60. package/dist/server.js.map +1 -1
  61. package/dist/stdio.d.ts +5 -1
  62. package/dist/stdio.d.ts.map +1 -1
  63. package/dist/stdio.js +10 -4
  64. package/dist/stdio.js.map +1 -1
  65. package/dist/tool-filters.d.ts +38 -0
  66. package/dist/tool-filters.d.ts.map +1 -0
  67. package/dist/tool-filters.js +129 -0
  68. package/dist/tool-filters.js.map +1 -0
  69. package/dist/tools/account.d.ts +3 -0
  70. package/dist/tools/account.d.ts.map +1 -0
  71. package/dist/tools/account.js +122 -0
  72. package/dist/tools/account.js.map +1 -0
  73. package/dist/tools/activities.d.ts +3 -0
  74. package/dist/tools/activities.d.ts.map +1 -0
  75. package/dist/tools/activities.js +140 -0
  76. package/dist/tools/activities.js.map +1 -0
  77. package/dist/tools/agent.d.ts.map +1 -1
  78. package/dist/tools/agent.js +72 -5
  79. package/dist/tools/agent.js.map +1 -1
  80. package/dist/tools/artifacts.d.ts +3 -0
  81. package/dist/tools/artifacts.d.ts.map +1 -0
  82. package/dist/tools/artifacts.js +97 -0
  83. package/dist/tools/artifacts.js.map +1 -0
  84. package/dist/tools/chat.d.ts +6 -0
  85. package/dist/tools/chat.d.ts.map +1 -0
  86. package/dist/tools/chat.js +192 -0
  87. package/dist/tools/chat.js.map +1 -0
  88. package/dist/tools/computers.d.ts.map +1 -1
  89. package/dist/tools/computers.js +300 -247
  90. package/dist/tools/computers.js.map +1 -1
  91. package/dist/tools/directory.d.ts +13 -0
  92. package/dist/tools/directory.d.ts.map +1 -0
  93. package/dist/tools/directory.js +88 -0
  94. package/dist/tools/directory.js.map +1 -0
  95. package/dist/tools/events.d.ts.map +1 -1
  96. package/dist/tools/events.js +66 -22
  97. package/dist/tools/events.js.map +1 -1
  98. package/dist/tools/executions.d.ts +3 -0
  99. package/dist/tools/executions.d.ts.map +1 -0
  100. package/dist/tools/executions.js +87 -0
  101. package/dist/tools/executions.js.map +1 -0
  102. package/dist/tools/guest.d.ts.map +1 -1
  103. package/dist/tools/guest.js +57 -17
  104. package/dist/tools/guest.js.map +1 -1
  105. package/dist/tools/input.d.ts.map +1 -1
  106. package/dist/tools/input.js +63 -5
  107. package/dist/tools/input.js.map +1 -1
  108. package/dist/tools/results.d.ts +30 -0
  109. package/dist/tools/results.d.ts.map +1 -0
  110. package/dist/tools/results.js +106 -0
  111. package/dist/tools/results.js.map +1 -0
  112. package/dist/tools/secrets.d.ts +58 -0
  113. package/dist/tools/secrets.d.ts.map +1 -0
  114. package/dist/tools/secrets.js +204 -0
  115. package/dist/tools/secrets.js.map +1 -0
  116. package/dist/tools/signals.d.ts +3 -0
  117. package/dist/tools/signals.d.ts.map +1 -0
  118. package/dist/tools/signals.js +116 -0
  119. package/dist/tools/signals.js.map +1 -0
  120. package/dist/tools/snapshots.d.ts.map +1 -1
  121. package/dist/tools/snapshots.js +213 -167
  122. package/dist/tools/snapshots.js.map +1 -1
  123. package/dist/tools/ssh.d.ts +3 -0
  124. package/dist/tools/ssh.d.ts.map +1 -0
  125. package/dist/tools/ssh.js +186 -0
  126. package/dist/tools/ssh.js.map +1 -0
  127. package/dist/tools/templates.d.ts.map +1 -1
  128. package/dist/tools/templates.js +9 -13
  129. package/dist/tools/templates.js.map +1 -1
  130. package/package.json +1 -1
package/README.md CHANGED
@@ -12,17 +12,54 @@ model looks at the screen and clicks what it sees.
12
12
 
13
13
  ## Install
14
14
 
15
- You need an API key from the dashboard — **Settings → API keys**, a `com_…`
16
- string. It is scoped to your account and it is every computer on it, so treat it
17
- the way you would treat a password.
15
+ MCP requires Node 20.3 or newer. The **TypeScript CLI** login setup requires
16
+ Node 22 or newer (`mandala-computer` on npm). Sign in once, then add the MCP
17
+ server:
18
18
 
19
- Node 20.3 or newer. There is nothing else to install: `npx` fetches the server
20
- the first time a client starts it.
19
+ ```sh
20
+ npm install -g mandala-computer
21
+ mandala login
22
+ claude mcp add mandala -- npx -y mandala-computer-mcp
23
+ ```
24
+
25
+ On Node 20, MCP can use an existing saved profile or an environment key.
26
+ The TypeScript CLI guides you through browser approval and saves an API key in
27
+ `~/.mandala/credentials.json`. MCP only reads that file; it never issues or
28
+ approves a device, writes credentials, or starts login automatically. The
29
+ Python distribution also provides a `mandala` command; use the TypeScript
30
+ CLI for `login`.
31
+
32
+ To select a separate profile and workspace:
33
+
34
+ ```sh
35
+ mandala login --profile Work --workspace Research
36
+ claude mcp add mandala -- npx -y mandala-computer-mcp --profile Work
37
+ ```
38
+
39
+ `--profile` takes precedence over `MANDALA_PROFILE`, then the saved default.
40
+ Names are case-sensitive. An explicit key or nonempty `MANDALA_API_KEY` takes
41
+ precedence over every profile and avoids accessing the credential store.
42
+ Empty explicit keys fail; an empty or whitespace-only environment key is absent.
43
+ You can still use a key from **Settings → API keys** with `MANDALA_API_KEY`.
44
+
45
+ Saved credentials require POSIX protection: a real directory owned by you with
46
+ mode `0700`, and an owned regular file with mode `0600` and one link. Symlinks,
47
+ hardlinks, unsafe permissions, malformed files and unsupported protection are
48
+ refused before any API request. Windows file loading is unsupported; explicit
49
+ or environment keys remain available. Only your home directory's store is read.
50
+
51
+ A saved profile also binds its API base URL. `--base-url` or `MANDALA_BASE_URL`
52
+ must match that stored base after canonicalization, including the entire path
53
+ prefix and port. An explicitly empty base flag is invalid with saved credentials.
54
+ Each stdio session resolves once: restart to pick up another saved key. Revoking
55
+ the key in **Settings → API keys** makes later calls fail; MCP does not switch
56
+ profiles, reread the file, or retry a refused action. Run login explicitly when
57
+ new credentials are needed.
21
58
 
22
59
  **Claude Code** — as a plugin, which installs the server and a skill together:
23
60
 
24
61
  ```sh
25
- export MANDALA_API_KEY=com_… # in the shell Claude Code starts from
62
+ # Run mandala login first, or export MANDALA_API_KEY in this shell.
26
63
  /plugin marketplace add mandalacomputer/mcp
27
64
  /plugin install mandala-computer@mandala
28
65
  ```
@@ -34,9 +71,9 @@ right answer at all, that it costs money until it is suspended or stopped, that
34
71
  which refusals are worth a second try. It is a description of *when and how*,
35
72
  not a second client; once the server is installed it stays out of the way.
36
73
  `MANDALA_MODEL_KEY`, if exported alongside, is passed through and turns on
37
- `run_agent`.
74
+ `run_agent` when the configured filters permit it.
38
75
 
39
- Or the server on its own, with the key inline:
76
+ To use an environment key instead of a saved profile:
40
77
 
41
78
  ```sh
42
79
  claude mcp add mandala -e MANDALA_API_KEY=com_… -- npx -y mandala-computer-mcp
@@ -49,8 +86,7 @@ claude mcp add mandala -e MANDALA_API_KEY=com_… -- npx -y mandala-computer-mcp
49
86
  "mcpServers": {
50
87
  "mandala": {
51
88
  "command": "npx",
52
- "args": ["-y", "mandala-computer-mcp"],
53
- "env": { "MANDALA_API_KEY": "com_…" }
89
+ "args": ["-y", "mandala-computer-mcp"]
54
90
  }
55
91
  }
56
92
  }
@@ -59,8 +95,11 @@ claude mcp add mandala -e MANDALA_API_KEY=com_… -- npx -y mandala-computer-mcp
59
95
  **Cursor, Windsurf and the rest** take the same three fields — `command`,
60
96
  `args`, `env` — in whichever file they keep their MCP servers in.
61
97
 
62
- Nothing is hosted and nothing is operated: your MCP client starts this as a
63
- subprocess, and it talks to `https://app.mandala.computer/api/v1` with your key.
98
+ Your MCP client starts this as a subprocess. It uses the saved profile’s API
99
+ base, or `https://app.mandala.computer/api/v1` by default with an environment key.
100
+ Programmatic local hosts can call `runStdio({ profile: 'Work' })`; the exported
101
+ `StdioConfig` allows a local key or profile. `createServer` still requires an
102
+ explicit API key, and `HttpConfig` has no local credential options.
64
103
 
65
104
  ## Use
66
105
 
@@ -95,6 +134,12 @@ entirely.
95
134
 
96
135
  ## The tools
97
136
 
137
+ This is the unfiltered inventory. The filters below can withhold tools from
138
+ both listing and calling; workflows in this README apply only when the needed
139
+ tools are available. The offline fixture checks exercised operation coverage;
140
+ verification against the live publication is a separate required CI gate.
141
+ Parameter and response-mode support remains a separate contract.
142
+
98
143
  **Choosing a machine** — `list_templates`, `list_sizes`, `list_computers`, `get_computer`,
99
144
  `use_computer`, `wait_for_computer`, `get_desktop_url`
100
145
 
@@ -105,9 +150,15 @@ entirely.
105
150
  **Driving the desktop** — `screenshot`, `click`, `type_text`, `press_key`,
106
151
  `scroll`, `drag`, `move_mouse`, `mouse_button`, `cursor_position`, `wait`
107
152
 
108
- **Inside the guest** — `exec`, `exec_poll`, `exec_kill`, `open_url`,
153
+ **Inside the guest** — `exec`, `exec_poll`, `exec_kill`, `get_execution`,
154
+ `read_execution_output`, `open_url`,
109
155
  `list_windows`, `window_action`, `read_clipboard`, `write_clipboard`,
110
- `read_file`, `write_file`
156
+ `read_file`, `write_file`, `list_directory`
157
+
158
+ **Retained versions** — `retain_execution_output`, `get_result`, `read_result_output`,
159
+ `delete_result`, `publish_artifact`, `get_artifact`, `read_artifact`, `delete_artifact`
160
+
161
+ **Passive metadata** — `list_activities`, `get_activity`, `get_activity_results`, `read_signals`
111
162
 
112
163
  **Being told rather than asking** — `wait_for_event`, `poll_events`,
113
164
  `wait_for_file_change`
@@ -121,14 +172,118 @@ entirely.
121
172
 
122
173
  **Building one** — `build_template`, `list_builds`, `get_build`, `watch_build`
123
174
 
175
+ **Account quota** — `get_account`
176
+
124
177
  **Spending** — `get_usage`
125
178
 
126
179
  **Being told somewhere else** — `list_webhooks`, `create_webhook`,
127
180
  `get_webhook`, `update_webhook`, `rotate_webhook_secret`, `test_webhook`,
128
181
  `list_webhook_deliveries`, `delete_webhook`
129
182
 
130
- **Delegating** — `run_agent`, registered only when a model key is present:
183
+ **SSH access** — `list_ssh_keys`, `add_ssh_key`, `remove_ssh_key`,
184
+ `get_computer_ssh`, `set_computer_ssh`. Keys belong to the person the API key
185
+ was issued to, not to the account, and are accepted by every computer with SSH
186
+ on, on every account where that person is an owner or member. A
187
+ workspace-scoped key can read keys but not add or remove them.
188
+
189
+ **Secret bindings** — `get_computer_secrets`, `set_computer_secrets`, and
190
+ `create_computer`'s `secrets`. Which of the account's secrets a computer
191
+ receives, at which revision, and where: as an environment variable (`env`) or as
192
+ a file under `/run/mandala-secrets/user/files` (`file`). Values never cross these
193
+ tools. A set replaces the whole list (`[]` removes every binding) and reaches the
194
+ guest at the computer's next start or restart; send the `version` a read
195
+ answered to have it refused with 409 if the list changed since. A secret bound
196
+ as a file is also rewritten on a running computer when its value is replaced.
197
+
198
+ **Delegating** — `run_agent`, `run_agent_chat`, registered only when a model key is present:
131
199
  `MANDALA_MODEL_KEY` on stdio, or the caller's own `X-Model-Key` header over HTTP.
200
+ Both must also survive the configured filters.
201
+
202
+ ### Tool filters
203
+
204
+ Set `MANDALA_READ_ONLY=1` to register only tools whose existing
205
+ `annotations.readOnlyHint` is exactly `true`. It accepts `1`, `true`, `yes`,
206
+ `on` for on and `0`, `false`, `no`, `off`, empty or unset for off, ignoring
207
+ surrounding whitespace and letter case. Any other value fails at startup.
208
+ Safety is not inferred from a tool's name or HTTP method: `read_file` and
209
+ `cursor_position` can wake and bill a computer, so they are withheld. So are
210
+ mixed read/write tools such as `wait_for_computer` and `snapshot_schedule`.
211
+ `screenshot`, `read_clipboard` and `list_windows` retain their read-only hints.
212
+
213
+ Set `MANDALA_TAGS=input,guest` to select a union of named tool groups. Names
214
+ are **lowercase only**, comma-separated, trimmed and deduplicated. Empty or
215
+ unset means no tag filter; empty comma-separated entries are ignored. An
216
+ unknown nonempty tag fails before stdio connects or HTTP starts listening,
217
+ with an error listing all valid tags.
218
+
219
+ | Tag | Tools |
220
+ | --- | --- |
221
+ | `account` | `get_account` |
222
+ | `computers` | `list_computers`, `get_computer`, `use_computer`, `wait_for_computer`, `get_desktop_url`, `list_sizes` |
223
+ | `lifecycle` | `create_computer`, `start_computer`, `stop_computer`, `suspend_computer`, `restart_computer`, `update_computer`, `clone_computer`, `delete_computer`, `move_computer`, `list_moves` |
224
+ | `input` | `screenshot`, `click`, `type_text`, `press_key`, `scroll`, `drag`, `move_mouse`, `mouse_button`, `cursor_position`, `wait` |
225
+ | `guest` | `exec`, `exec_poll`, `exec_kill`, `open_url`, `list_windows`, `window_action`, `read_clipboard`, `write_clipboard` |
226
+ | `files` | `list_directory`, `read_file`, `write_file`, `wait_for_file_change` |
227
+ | `executions` | `get_execution`, `read_execution_output` |
228
+ | `results` | `get_activity_results`, `retain_execution_output`, `get_result`, `read_result_output`, `delete_result` |
229
+ | `artifacts` | `publish_artifact`, `get_artifact`, `read_artifact`, `delete_artifact` |
230
+ | `snapshots` | All snapshot tools listed above, including `get_retention` |
231
+ | `templates` | `list_templates`, all your-own-template tools and all build tools listed above |
232
+ | `events` | `wait_for_event`, `poll_events`, `wait_for_file_change` |
233
+ | `usage` | `get_usage` |
234
+ | `webhooks` | All webhook tools listed above |
235
+ | `ssh` | All SSH tools listed above |
236
+ | `secrets` | `get_computer_secrets`, `set_computer_secrets` |
237
+ | `agent` | `run_agent`, `run_agent_chat` |
238
+ | `activities` | `list_activities`, `get_activity`, `get_activity_results` |
239
+ | `signals` | `read_signals` |
240
+
241
+ The selected tags form a union, then intersect with read-only and the existing
242
+ lifecycle and model-key restrictions. `MANDALA_NO_LIFECYCLE=1` still withholds
243
+ its five tools even when their tags are selected; selecting `agent` cannot
244
+ enable either agent tool without a per-session model key. Both agent tools are
245
+ withheld by read-only, but remain available with lifecycle disabled alone.
246
+ `MANDALA_TAGS=files MANDALA_READ_ONLY=1` exposes only `list_directory`.
247
+ An `agent` selection with read-only is a valid empty tool list.
248
+ `activities,results` includes `get_activity_results` once; `signals` is separate
249
+ from the active `events` socket. Filters never grant API privileges: the API
250
+ checks current member/owner and workspace authorization on every request.
251
+
252
+ `use_computer` is not read-only. When it is withheld, pass `computer_id`
253
+ explicitly, or bind a computer with `MANDALA_COMPUTER_ID` at stdio startup.
254
+ HTTP callers must supply their own computer selection. These variables apply
255
+ to both transports and the plugin forwards them. Embedders can pass
256
+ `readOnly: true` and `tags: ['input', 'guest']` in `ServerConfig`; the server
257
+ does not read the environment itself.
258
+
259
+ ### Current account quota
260
+
261
+ `get_account` takes no arguments and reads `GET /api/v1/account` once with the
262
+ caller's account credential. Viewer or stronger access is required. It reports
263
+ account-wide aggregates, including for a workspace-scoped key, without resource
264
+ identities. It needs no selected computer or model key, opens no event stream,
265
+ and remains available with read-only and no-lifecycle filters. Select the
266
+ `account` tag to expose it on its own.
267
+
268
+ The report includes the effective plan, pool ceilings, per-computer maxima,
269
+ Windows capability, current consumption and remaining quota. Configured CPU and
270
+ disk include kept computers regardless of power state; running or reserved RAM
271
+ includes current reservations. CPU, MB, GB and snapshot bytes retain the API's
272
+ units. `get_usage` separately reports historical metered consumption over time.
273
+
274
+ Quota is **advisory**: `observed_at` is an observation time, not a reservation or
275
+ consistency token. Later create, resize, start or snapshot requests can still be
276
+ refused, and existing 402 messages remain unchanged. Snapshot headroom is against
277
+ **indexed stored bytes**; it does not include in-flight capture reservations and
278
+ does not establish that a new capture will fit.
279
+
280
+ `complete.computers` and `complete.snapshots` are independent. An incomplete group
281
+ has `null` for all its consumption and remaining figures, meaning **unknown**,
282
+ while the other group can retain numeric values. Complete zero usage and zero
283
+ remaining quota remain numeric zeros. Verified plan ceilings remain available in
284
+ a partial report, including a no-plan account that still has retained usage.
285
+ Malformed reports and HTTP failures remain errors, never an empty account.
286
+ The tool prints unknown and advisory guidance before the projected public fields.
132
287
 
133
288
  ## Things worth knowing
134
289
 
@@ -157,9 +312,11 @@ time — `screenshot`, `click`, `screenshot` — puts an image in the calling
157
312
  model's context for every step. `run_agent` hands a task in plain language to
158
313
  the platform's own loop instead, which screenshots, decides and clicks inside
159
314
  the platform and answers with a sentence and the list of what it did. It is
160
- registered only when a model key is present (see [Configuration](#configuration)),
161
- bills that key for every step, and `max_steps` is the spending cap as much as
162
- the loop bound.
315
+ registered only when a model key is present (see [Configuration](#configuration))
316
+ and bills that key for the run. `max_steps` bounds the WORK rather than the
317
+ bill: a step is one action on the desktop, one model reply can ask for several
318
+ and spends a step on each, a paused turn costs tokens and no step, and not every
319
+ step takes a screenshot. It defaults to 20 and is capped at 100 here.
163
320
 
164
321
  **A screenshot is how you find out what the screen looks like.** A click that
165
322
  landed and a click that did nothing produce the same tool result, so a model
@@ -355,6 +512,107 @@ foreground comes back as a timeout, with the work still going inside the guest
355
512
  and its output unreadable. With a handle you get the exit code and the output,
356
513
  and `exec_kill` stops it.
357
514
 
515
+ **Stable background reads.** When an accepted `exec` returns `execution_id`,
516
+ use `get_execution` for its last observed `running`, `exited` or `lost` state.
517
+ Only `exited` carries an `ended_at` and signed `exit_code`; `running` does not
518
+ prove the computer is awake, and `lost` establishes neither success nor failure.
519
+ Older replies can omit the ID. A malformed supplied ID leaves the accepted
520
+ command and its valid PID usable, but supplies no stable identity: do not replay
521
+ the command to obtain one. PID polls/kills never reconstruct this association.
522
+
523
+ `read_execution_output` takes that ID and **both** `stdout_offset` and
524
+ `stderr_offset` byte positions. Start each reader at zero, then pass its own
525
+ returned positions. For example:
526
+
527
+ ```json
528
+ {"execution_id":"exec_0123456789abcdef0123456789abcdef","stdout_offset":0,"stderr_offset":0,"limit":4096}
529
+ ```
530
+
531
+ Each stream is bounded to 4,096 bytes by default, at most 16,384. Complete
532
+ lossless UTF-8 appears as `stdout`/`stderr` with its BOM preserved. NUL, binary
533
+ and split UTF-8 chunks remain exact canonical `stdout_b64`/`stderr_b64`; nothing
534
+ is trimmed or replaced. `stdout_more` and `stderr_more` are independent, and
535
+ false means EOF at that moment, not that the command has finished.
536
+
537
+ The separate `diagnostic`/`diagnostic_b64` repeats on every read. At most 4,096
538
+ of its up-to-65,536 available bytes are displayed: inspect
539
+ `diagnostic_available_bytes`, `diagnostic_displayed_bytes`, and
540
+ `diagnostic_display_truncated`. The independent `diagnostic_truncated` flag is
541
+ the platform's capture limitation. Diagnostics never advance either cursor.
542
+ Neither new tool consumes output from another reader or the shared `exec_poll`
543
+ cursor, and both carry read-only, non-destructive, idempotent annotations.
544
+
545
+ These are single requests with cancellation, no automatic resume, retry,
546
+ watcher, capture or command replay. Metadata reads no guest files. Output reads
547
+ perform guest I/O without refreshing activity and are unsuitable for passive
548
+ Activities/history. Files are mutable guest data, not retained artifacts;
549
+ handles can vanish on restart, replacement or cleanup, and observed exits
550
+ expire after ten minutes. An unavailable read is an error, not empty output.
551
+ Every new-tool result is bounded to 256 KiB of serialized data.
552
+
553
+ **Explicit immutable retained versions.** `retain_execution_output` accepts an
554
+ `execution_id` and captures one version of its volatile output with one POST.
555
+ It performs guest I/O, without resuming or replaying the command. Its optional
556
+ `max_bytes_per_stream` defaults to 1 MiB (maximum 4 MiB); `retention_seconds`
557
+ defaults to 86400 (maximum 604800). Diagnostics are separate, up to 64 KiB.
558
+ Each capture creates a version; there is no automatic capture or retry.
559
+
560
+ `get_result` reads finite metadata by `result_id`. `read_result_output` requires
561
+ `result_id`, `stream` (`stdout`, `stderr` or `diagnostic`) and `offset`, with
562
+ `limit` defaulting to 4096 and capped at 16384 bytes. It reads one independent
563
+ page, with exact `offset`, `next_offset`, `eof` and returned `bytes` count.
564
+ Content is lossless `text` (BOM preserved) or canonical `base64` for binary,
565
+ controls and split UTF-8. EOF means the end of this retained prefix, not task
566
+ completion. A page alone does not verify the full manifest hash.
567
+ `delete_result` deletes that version once; a repeated 404 remains unavailable.
568
+
569
+ Existing synchronous `exec` accepts `retain_output: true` or a strict object
570
+ with those same two options. False or absence leaves default behavior alone.
571
+ It cannot be combined with `background: true`. A canonical returned `result_id`
572
+ confirms optional retention; no execution ID is fabricated. Missing, malformed
573
+ or unsupported optional metadata leaves the command outcome unchanged and does
574
+ not authorize replay. Retained-prefix truncation and upstream response
575
+ truncation are distinct. Synchronous results have no diagnostic stream; an
576
+ explicit diagnostic read can return 409.
577
+
578
+ **Nominated file versions.** `publish_artifact` requires an absolute `path`,
579
+ `expected_size` and `expected_sha256` supplied by the caller. For example:
580
+
581
+ ```json
582
+ {"path":"/tmp/empty.txt","expected_size":0,"expected_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}
583
+ ```
584
+
585
+ Paths preserve legal Unicode, spaces and Linux backslashes; Windows drive and
586
+ UNC paths are also accepted, subject to the platform's current OS proof.
587
+ Publication performs one nominated guest-file capture, without a client-side
588
+ stat, list, read, hash or exec preflight. `max_bytes` defaults to 8 MiB and is at
589
+ most 64 MiB; expected size must fit. Retention has the bounds above. Optional
590
+ `execution_id` records verified **caller selection**, not proof the execution
591
+ created the file. Every publication creates an independent immutable version.
592
+
593
+ `get_artifact` reads metadata. `read_artifact` first reads that metadata and,
594
+ only if the complete size fits `max_bytes` (default 4096, maximum 16384), reads
595
+ the entire retained object and verifies its exact length and SHA-256. Over-cap
596
+ objects return metadata with a refusal before any content request; use an SDK
597
+ whole download with an adequate cap. There are no partial artifacts, Range
598
+ requests, local destinations, filenames, image/HTML previews or guest fallbacks.
599
+ Verified content uses the same lossless `text`/`base64` presentation.
600
+ `delete_artifact` deletes only that stored version, not the guest file; repeat
601
+ 404s remain truthful failures.
602
+
603
+ All eight retained tools resolve the computer selection once. Reads are passive
604
+ but require current authorization and scope availability; retained bytes can be
605
+ unavailable when the host cannot verify access, after expiry or deletion, or
606
+ when storage is unavailable. They never wake a guest or substitute live output.
607
+ New operations have one 90-second budget across headers, bodies, and both
608
+ artifact requests. An MCP client can impose an earlier deadline. Cancellation,
609
+ redirects and malformed responses never trigger a retry; a lost publication
610
+ response means commitment is **unconfirmed**, not undone. Each entire serialized
611
+ retained tool result is bounded to 256 KiB. Reads are marked read-only;
612
+ capture/publication create new versions; deletes are destructive with an
613
+ idempotent deletion effect. Activities result detail remains outside this MCP
614
+ runtime.
615
+
358
616
  Past about **two minutes** it does not even come back as a timeout. A proxy in
359
617
  front of the platform abandons a request that has produced no response for
360
618
  roughly that long and answers 524, which arrives as `GatewayTimeoutError` —
@@ -455,6 +713,66 @@ about storage instead.
455
713
  guest agent answers 409. The platform's own error messages come through
456
714
  unedited, because they are written to be acted on.
457
715
 
716
+ HTTP failures preserve their actual response status. An unsupported method on a
717
+ known path is `MethodNotAllowedError` (405), with the received `Allow` value when
718
+ available. A missing computer, snapshot, route or guest file remains
719
+ `NotFoundError` (404); other guest failures keep their own status and message.
720
+ Neither response causes an automatic retry or method switch.
721
+
722
+ Embedders can inspect optional diagnostics on every `APIError`:
723
+
724
+ ```ts
725
+ import { Api, APIError, MethodNotAllowedError } from 'mandala-computer-mcp';
726
+
727
+ const api = new Api(process.env.MANDALA_API_KEY!);
728
+ try {
729
+ await api.json('GET', 'account');
730
+ } catch (error) {
731
+ if (error instanceof APIError) {
732
+ console.error({ status: error.status, requestId: error.requestId,
733
+ reason: error.reason, allow: error.allow,
734
+ wwwAuthenticate: error.wwwAuthenticate });
735
+ if (error instanceof MethodNotAllowedError) {
736
+ // Inspect error.allow before correcting the request method.
737
+ }
738
+ }
739
+ }
740
+ ```
741
+
742
+ `requestId` uses a nonblank `X-Request-ID` header first, then the top-level
743
+ `request_id` body field. It is an opaque diagnostic, never an idempotency key.
744
+ The raw body remains available on `error.body`, including any differing body ID
745
+ and nested chat accounting. `allow` and `wwwAuthenticate` come only from received
746
+ headers. HEAD failures can carry these fields with no body. Older servers,
747
+ intermediaries and connection failures may supply none of them. Existing
748
+ constructor arguments retain their meanings; an optional trailing
749
+ `APIErrorMetadata` object adds these three fields.
750
+
751
+ MCP error results include supplied `reason`, `request_id`, `allow`,
752
+ `www_authenticate` and `retry_after_ms` as labelled JSON metadata. Diagnostic
753
+ strings are limited to 128, 256, 512 and 512 characters respectively; an
754
+ oversized field is omitted with a notice, so a shortened Allow is never
755
+ presented as complete. Tool-specific warnings about partial work, retained
756
+ publication, execution reads and explicit template continuation still apply.
757
+ Serialized JSON object or array prefixes are not displayed as error prose.
758
+ Valid scalar error messages retain their wording; embedders still have the
759
+ original `APIError.message` and `APIError.body` for diagnostics. Native agent
760
+ error frames retain their supplied numeric status even without a reason or
761
+ request ID, independently of the successful HTTP stream carrying them.
762
+
763
+ For a 401, `missing` means a platform credential was not supplied; `invalid`
764
+ means the supplied platform credential was not accepted; `revoked` means its
765
+ authority no longer holds. Unknown reasons stay visible. An unclassified 401
766
+ alone does not identify whether the account key or model key was refused,
767
+ including a nested chat failure. A 403 remains a permission or authority
768
+ refusal. Inspect recorded work before another run; no key fallback, login or
769
+ automatic replay is performed. Nested error reasons and in-band stream errors
770
+ do not grant permission to replay a partly completed run.
771
+
772
+ For desktop events, use the exact returned `events_url`, including its desktop
773
+ capability, in a WebSocket client. A REST Bearer key alone is not sufficient;
774
+ the HTTP events response provides JSON guidance rather than another login flow.
775
+
458
776
  **Desktop links are credentials.** `get_desktop_url` returns the watch-only URL
459
777
  by default — the platform drops input on that socket, so it is safe to hand to
460
778
  somebody. `control: true` returns the full-control one, which is root-equivalent
@@ -556,7 +874,9 @@ for whatever is checking that the process is up.
556
874
  bearer token and is used only for their session; there is no store, and nothing
557
875
  outlives a session but a digest of the key — kept so that a later request can be
558
876
  shown to come from the same holder, which means a leaked session id on its own
559
- is not enough to drive somebody else's desktop.
877
+ is not enough to drive somebody else's desktop. HTTP startup and requests never
878
+ read the operator's credential store, `MANDALA_API_KEY` or `MANDALA_PROFILE`.
879
+ `--profile` is local-only and ignored with `--http`.
560
880
 
561
881
  That is also why anyone can run their own: point the same container at the same
562
882
  API and it works, with no secret to provision.
@@ -579,17 +899,20 @@ naming the fix.
579
899
 
580
900
  | Variable | Meaning |
581
901
  | --- | --- |
582
- | `MANDALA_API_KEY` | `com_…` from Settings → API keys. Required on stdio; over HTTP each caller sends their own. |
583
- | `MANDALA_BASE_URL` | Defaults to `https://app.mandala.computer/api/v1`. |
902
+ | `MANDALA_API_KEY` | Optional local API key, taking precedence over saved credentials. Ignored by HTTP; each caller sends their own bearer token. |
903
+ | `MANDALA_PROFILE` | Local saved profile; `--profile` overrides it. Otherwise the file default is selected. Ignored by HTTP. |
904
+ | `MANDALA_BASE_URL` | With a saved profile, must match its stored base. Otherwise defaults to `https://app.mandala.computer/api/v1`. |
584
905
  | `MANDALA_COMPUTER_ID` | Bind a computer at startup, so `use_computer` is not needed. **stdio only** — under `--http` it is ignored rather than bound into every caller's session, since it names a machine on the operator's account. |
585
- | `MANDALA_MODEL_KEY` | An Anthropic key. Enables `run_agent`, which runs the platform's own loop on that key. **stdio only** — under `--http` each caller sends their own as `X-Model-Key`, and this variable is ignored. |
906
+ | `MANDALA_MODEL_KEY` | An Anthropic key. Enables `run_agent` and `run_agent_chat` when filters permit them, which runs the platform's own loop on that key. **stdio only** — under `--http` each caller sends their own as `X-Model-Key`, and this variable is ignored. |
586
907
  | `MANDALA_NO_LIFECYCLE` | `1`, `true`, `yes` or `on` withholds `create_computer`, `clone_computer`, `clone_snapshot`, `delete_computer` and `delete_snapshot` — every tool that makes a computer or destroys one. `0`, `false`, `no`, `off` or unset leaves them registered. Any other value is **refused at startup** rather than read as off: a typo here would otherwise leave those tools in place on a server whose operator believes they are gone. The `--no-lifecycle` flag reads the same vocabulary and refuses the same way, except that it has no spelling for *unset*: `--no-lifecycle=` is refused rather than ignored, so a launcher template whose variable did not expand stops instead of quietly leaving the tools registered. |
587
908
  | `PORT`, `HOST` | For `--http`. Default `3000`, `127.0.0.1`. |
909
+ | `MANDALA_READ_ONLY` | Keep only tools annotated `readOnlyHint: true`; strict boolean parsing as described under Tool filters. |
910
+ | `MANDALA_TAGS` | Comma-separated lowercase tool tags; see Tool filters for the inventory and intersection rules. |
588
911
  | `MANDALA_ALLOWED_HOSTS`, `MANDALA_ALLOWED_ORIGINS` | Comma-separated. Which `Host` and `Origin` values this server answers to. On a loopback bind the host list defaults to the address it was given, so DNS-rebinding protection is on without configuration; set this when serving under a name. |
589
912
 
590
- Every one of these but the model key has a flag as well, and a flag overrides
913
+ Every one of these but the model key and the two tool filters has a flag as well, and a flag overrides
591
914
  the environment: `--http`, `--port`, `--host`, `--base-url`, `--computer`,
592
- `--allowed-hosts`, `--allowed-origins`, `--no-lifecycle`, plus `--help` and
915
+ `--allowed-hosts`, `--allowed-origins`, `--no-lifecycle`, local `--profile`, plus `--help` and
593
916
  `--version`. `--key` exists for a caller launching several servers under
594
917
  different keys, and warns when used, because an argument vector is readable by
595
918
  `ps`, lands in shell history and is recorded verbatim by any exec audit —
@@ -601,17 +924,116 @@ it when a stretch of pixel work would otherwise cost the calling model a
601
924
  screenshot per step — ten clicks stop being ten images. It bills your Anthropic
602
925
  key, and the platform never stores that key.
603
926
 
927
+ ### Passive directories, activity history and signals
928
+
929
+ `list_directory` takes an exact absolute guest `path`. Unicode, spaces and
930
+ punctuation survive query encoding. It requires an already running computer,
931
+ does not resume it or extend its idle timer, and still contacts the guest with
932
+ ordinary rate/capacity admission. The result preserves `path`, `entries` with
933
+ `name`, `type` and optional `size_bytes`, `truncated` and `skipped`. An unavailable
934
+ entry has no inferred type or zero size. Symlinks are not followed, and a final
935
+ symlink directory is refused. No file content is read. A partial listing is an
936
+ unordered subset: at most 512 examined entries/128 KiB, with no continuation
937
+ token. Narrow the path when names are omitted; do not automatically rescan.
938
+
939
+ `list_activities` returns one newest-first history page with a fixed watermark.
940
+ Pass an opaque `cursor` for continuation, or `changes:true` with a cursor for
941
+ late final updates to older rows. False/absent `changes` is omitted on the wire;
942
+ there is no `limit` argument. Preserve `next_cursor`, `changes_cursor`, `gap`
943
+ and health fields. A gap requires refreshing history. The tools keep no cursor
944
+ cache and never drain pages automatically. `get_activity` takes an `activity_id`
945
+ shaped as `act_` plus 32 lowercase hex digits, a request identity rather than an
946
+ idempotency key. Recorded state, scope, revision, timestamps, execution identity
947
+ and status remain distinct: accepted background work is not finished execution,
948
+ and a dispatched error may have had effects. History is selected, retained,
949
+ best-effort metadata, not all guest work or an agent identity.
950
+
951
+ `get_activity_results` returns at most eight newest metadata links, preserving
952
+ revision, availability, association, byte/truncation metadata and observations.
953
+ `more:true` has no continuation cursor and does not mean all versions were
954
+ returned. An unavailable item is not an empty available item. Caller-selected
955
+ artifact association does not prove creation or command success. This tool
956
+ never follows a link to content, files, captures, downloads or execution polling.
957
+
958
+ `read_signals` reads one passive daemon page, independently of the active event
959
+ socket. Omit `since` or send an empty string for a head-only baseline with no
960
+ replay. Optional `limit` is 1–100; omitting it uses the API default 50. Preserve
961
+ the returned `cursor` even when `events` is empty: filtered-out rows may advance
962
+ it. Retention is ephemeral; expired/restarted/migrated cursors can return an
963
+ explicit reset gap. Baselines and gaps do not prove there was no earlier work
964
+ or that a task succeeded. Unsupported (501) and unavailable (503) responses
965
+ remain errors, never empty history or new checkpoints. There is no watcher,
966
+ guest action, retry loop or automatic checkpoint cache in this tool.
967
+
968
+ ### JSON chat with your own Anthropic key
969
+
970
+ `run_agent_chat` drives the selected computer using the same BYOK Anthropic loop
971
+ as `run_agent`. It accepts a nonempty array of textual OpenAI-shaped `messages`,
972
+ including string content or arrays of `{type:"text",text:"..."}` parts. The
973
+ **last user message** supplies the task; system messages supply standing
974
+ instructions. Earlier user/assistant conversation is not replayed. Optional
975
+ `model` is sent unchanged and must name an Anthropic model. `max_steps` is 1–100,
976
+ default 20. This is computer control, not hosted general-purpose inference or a
977
+ chat UI; it adds no key storage or model billing service.
978
+
979
+ This tool deliberately sends `stream:false` and uses the JSON response so the
980
+ result preserves the underlying `agent.stop`, step count and token usage.
981
+ Only explicit `end_turn` consistent with the completion's finish reason can
982
+ report success. Limits, refusal, missing or conflicting terminal fields remain
983
+ errors with valid partial results. Nested failures preserve the agent computer,
984
+ recorded steps and native usage, including separate cache-read and cache-write
985
+ token counts, alongside OpenAI-shaped aggregate usage. Malformed native detail
986
+ is explicitly marked incomplete. A failed call may include completed, billed
987
+ work: inspect it before deciding what remains, and do not automatically replay.
988
+ Neither agent tool starts the computer or switches endpoints after a failure.
989
+
990
+ The caller supplies the model key through `MANDALA_MODEL_KEY` on stdio or their
991
+ own `X-Model-Key` header on HTTP. The account Authorization remains separate;
992
+ HTTP never falls back to the operator's model key. Waiting heartbeats count
993
+ notifications, not completed actions. Long-running clients need a
994
+ `progressToken` plus `resetTimeoutOnProgress`; otherwise choose a smaller
995
+ `max_steps`. That bound is neither a time cap nor a spend cap.
996
+
604
997
  ## Development
605
998
 
606
999
  ```sh
607
- npm install
608
- npm test # vitest, plus the surface check below
1000
+ npm ci
1001
+ npx vitest run # deterministic offline tests, including the synthetic contract
1002
+ npm test # offline tests plus the separate private mirror check
609
1003
  npm run build
610
1004
  npm run lint
611
1005
  ```
612
1006
 
613
- CI runs the suite on Node 20, 22, 24 and 26 — the floor `package.json`
614
- declares and the ceiling a current `npx` will actually use.
1007
+ CI runs the offline suite on Node 20, 22, 24 and 26. A separate Node 22
1008
+ `published-openapi` job runs on pull requests and main pushes:
1009
+
1010
+ ```sh
1011
+ npx vitest run --config vitest.openapi.config.ts
1012
+ ```
1013
+
1014
+ It anonymously fetches the fixed publication at
1015
+ `https://app.mandala.computer/api/docs/openapi.json` once, with a 20-second
1016
+ overall deadline and an 8 MiB body ceiling, then exercises the real MCP tools
1017
+ and requires request evidence for every published `/api/v1` operation. Every
1018
+ exercise variant must succeed and dispatch a request, including later variants
1019
+ of a tool that already dispatched successfully.
1020
+ Non-v1 operations are explicitly excluded and reported. The check uses OpenAPI
1021
+ server inheritance and literal-path precedence, not the local route allowlist.
1022
+ Each path is appended to its effective server base; repeated prefixes are not
1023
+ removed. Fully prefixed paths work with an absent or root server.
1024
+ It fails on a blocked fetch, redirect, invalid document or missing operation;
1025
+ there is no fixture fallback, credential requirement or skip branch.
1026
+
1027
+ The committed OpenAPI fixture is synthetic, assembled from the public MCP route
1028
+ inventory, and is never presented as a downloaded publication. Offline green
1029
+ proves deterministic implementation coverage only. Anonymous CI at
1030
+ [commit 1b04314](https://github.com/mandalacomputer/mcp/commit/1b04314e84f5701e2e15c65cbb1c44a0c14a4948)
1031
+ returned HTTP 200 and matched all 56 operations in the fetched v1 contract using
1032
+ 123 requests from 83 tools. That result applies to that commit and publication;
1033
+ every subsequent head must pass the gate independently. These counts are
1034
+ observations, never fixed thresholds or exemptions. A blocked or red public gate
1035
+ must not be merged. Parameters and response modes remain separate contracts,
1036
+ with documented parameter exceptions and JSON-only chat support.
615
1037
 
616
1038
  ### Where the platform's rules live
617
1039
 
@@ -624,17 +1046,20 @@ change to the platform's route table, not a wider pass-through here.
624
1046
 
625
1047
  The platform allowlists every route `/api/v1` will answer and 404s the rest.
626
1048
  `test/allowlist.ts` mirrors that table, and the tests assert two things: that
627
- every call this server can make lands on an allowlisted route, and that the gap
628
- between the platform's surface and this server's coverage is *exactly* the set
629
- written down in `UNIMPLEMENTED`. A route added upstream becomes a failing test
630
- here rather than a feature nobody noticed.
1049
+ every successful exercised call lands on an allowlisted route, and that no
1050
+ mirrored operation remains unexercised (`UNIMPLEMENTED` is empty). Tool names
1051
+ and exercise entries must match in both directions, and every tool must make
1052
+ an observed HTTP request during its callback. The independent public gate also
1053
+ detects a new published operation when the mirror has not yet been updated.
631
1054
 
632
1055
  `npm run check:surface` goes further and diffs the mirror against the platform's
633
- own route table, whenever the platform repository happens to be checked out next
634
- door — or wherever `MANDALA_PLATFORM_REPO` points. Without it the script says
635
- it is skipping and exits 0, which is what it does for anyone outside the
636
- platform team; the diff is enforced from the platform's own CI, which checks
637
- this repository out beside itself and runs the same script.
1056
+ published surface manifest — a file the platform generates from its own tables
1057
+ and commits like a lockfile — whenever the platform repository happens to be
1058
+ checked out next door, or wherever `MANDALA_PLATFORM_REPO` points. Without it
1059
+ the script says it is skipping and exits 0, which is what it does for anyone
1060
+ outside the platform team; a manifest it cannot read is a failure, never a
1061
+ comparison of nothing. The diff is enforced from the platform's own CI, which
1062
+ checks this repository out beside itself and runs the same script.
638
1063
 
639
1064
  ```
640
1065
  check:surface — the mirror matches the platform (N routes, N parameters, from …).
package/dist/api.d.ts CHANGED
@@ -145,6 +145,14 @@ export declare class Api {
145
145
  * Routes where an empty body IS the answer use `send`.
146
146
  */
147
147
  json<T = unknown>(method: string, path: string, opts?: RequestOptions): Promise<T>;
148
+ /** Existing exec decoding plus observed status, used only to confirm optional retention. */
149
+ jsonWithStatus<T>(method: string, path: string, opts?: RequestOptions): Promise<{
150
+ status: number;
151
+ value: T;
152
+ }>;
153
+ /** Complete bounded responses for immutable retained protocols; never a successful prefix. */
154
+ boundedBytes(method: string, path: string, limit: number, status: number, opts?: RequestOptions): Promise<BoundedBytes>;
155
+ boundedJson(method: string, path: string, limit: number, status: number, opts?: RequestOptions): Promise<unknown>;
148
156
  /**
149
157
  * A request whose answer may legitimately be nothing.
150
158
  *
@@ -196,4 +204,8 @@ export declare class Api {
196
204
  export declare function causes(err: unknown, depth?: number): Generator<Record<string, unknown>>;
197
205
  /** The filename the platform put on a download, if it put one there. */
198
206
  export declare function filenameFrom(disposition: string | null): string | undefined;
207
+ export type BoundedBytes = {
208
+ bytes: Uint8Array;
209
+ headers: Record<string, string>;
210
+ };
199
211
  //# sourceMappingURL=api.d.ts.map