mandala-computer-mcp 0.7.0 → 0.9.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.
- package/README.md +232 -29
- package/dist/api.d.ts +6 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +13 -3
- package/dist/api.js.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +76 -1
- package/dist/cli.js.map +1 -1
- package/dist/credentials.d.ts +55 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +104 -2
- package/dist/credentials.js.map +1 -1
- package/dist/errors.d.ts +57 -16
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +112 -42
- package/dist/errors.js.map +1 -1
- package/dist/format.d.ts +17 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +27 -4
- package/dist/format.js.map +1 -1
- package/dist/http.d.ts +58 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +713 -57
- package/dist/http.js.map +1 -1
- package/dist/paths.d.ts +18 -11
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +31 -14
- package/dist/paths.js.map +1 -1
- package/dist/secret-store.d.ts +2 -1
- package/dist/secret-store.d.ts.map +1 -1
- package/dist/secret-store.js +6 -1
- package/dist/secret-store.js.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +24 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +6 -0
- package/dist/session.js.map +1 -1
- package/dist/stdio.d.ts.map +1 -1
- package/dist/stdio.js +9 -1
- package/dist/stdio.js.map +1 -1
- package/dist/tool-filters.d.ts +1 -1
- package/dist/tool-filters.d.ts.map +1 -1
- package/dist/tool-filters.js +3 -0
- package/dist/tool-filters.js.map +1 -1
- package/dist/tools/account.d.ts.map +1 -1
- package/dist/tools/account.js +80 -6
- package/dist/tools/account.js.map +1 -1
- package/dist/tools/agent.js +12 -23
- package/dist/tools/agent.js.map +1 -1
- package/dist/tools/chat.d.ts.map +1 -1
- package/dist/tools/chat.js +56 -7
- package/dist/tools/chat.js.map +1 -1
- package/dist/tools/computers.d.ts.map +1 -1
- package/dist/tools/computers.js +93 -8
- package/dist/tools/computers.js.map +1 -1
- package/dist/tools/guest.d.ts.map +1 -1
- package/dist/tools/guest.js +28 -3
- package/dist/tools/guest.js.map +1 -1
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +98 -48
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/operations.d.ts +7 -2
- package/dist/tools/operations.d.ts.map +1 -1
- package/dist/tools/operations.js +29 -6
- package/dist/tools/operations.js.map +1 -1
- package/dist/tools/secrets.d.ts.map +1 -1
- package/dist/tools/secrets.js +80 -25
- package/dist/tools/secrets.js.map +1 -1
- package/dist/tools/snapshots.d.ts.map +1 -1
- package/dist/tools/snapshots.js +21 -8
- package/dist/tools/snapshots.js.map +1 -1
- package/dist/tools/templates.js +2 -2
- package/dist/tools/templates.js.map +1 -1
- package/dist/tools/webhooks.js +1 -1
- package/dist/tools/webhooks.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,8 @@ claude mcp add mandala -- npx -y mandala-computer-mcp --profile Work
|
|
|
40
40
|
Names are case-sensitive. An explicit key or nonempty `MANDALA_API_KEY` takes
|
|
41
41
|
precedence over every profile and avoids accessing the credential store.
|
|
42
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
|
|
43
|
+
You can still use a key from **Settings → Credentials → API keys** with
|
|
44
|
+
`MANDALA_API_KEY`.
|
|
44
45
|
|
|
45
46
|
Saved credentials require POSIX protection: a real directory owned by you with
|
|
46
47
|
mode `0700`, and an owned regular file with mode `0600` and one link. Symlinks,
|
|
@@ -52,9 +53,29 @@ A saved profile also binds its API base URL. `--base-url` or `MANDALA_BASE_URL`
|
|
|
52
53
|
must match that stored base after canonicalization, including the entire path
|
|
53
54
|
prefix and port. An explicitly empty base flag is invalid with saved credentials.
|
|
54
55
|
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
|
|
56
|
-
profiles, reread the file, or retry a refused action. Run login
|
|
57
|
-
new credentials are needed.
|
|
56
|
+
the key in **Settings → Credentials → API keys** makes later calls fail; MCP
|
|
57
|
+
does not switch profiles, reread the file, or retry a refused action. Run login
|
|
58
|
+
explicitly when new credentials are needed.
|
|
59
|
+
|
|
60
|
+
A saved profile can also carry a default workspace, set with
|
|
61
|
+
`mandala workspaces use <workspace>` (and removed with `--clear`); the CLI keeps
|
|
62
|
+
it in `~/.mandala/defaults.json`, beside `credentials.json`, which it never
|
|
63
|
+
changes. Started from that profile, the secret tools (`list_secrets`,
|
|
64
|
+
`get_secret`, `create_secret`, `set_secret`, `replace_secret`,
|
|
65
|
+
`delete_secret`) use it when a call leaves out `workspace_id`; an explicit
|
|
66
|
+
`workspace_id` always wins. Their descriptions say so and name the workspace,
|
|
67
|
+
`list_secrets` says when the default chose the scope, and startup prints it on
|
|
68
|
+
stderr. MCP only reads that file, once at startup, with the same checks as
|
|
69
|
+
`credentials.json`. It is not applied when the profile's key is confined to a
|
|
70
|
+
workspace (that key's own scope applies), when the default was saved for
|
|
71
|
+
another account than the profile is logged in to now, or with an explicit or
|
|
72
|
+
environment key. A file that cannot be read (not valid JSON, another version,
|
|
73
|
+
or readable by others) is noted on stderr: `list_secrets` and `get_secret` then
|
|
74
|
+
use account-wide, but `create_secret`, `set_secret`, `replace_secret` and
|
|
75
|
+
`delete_secret` refuse a call without `workspace_id`, sending nothing, rather
|
|
76
|
+
than act account-wide; pass `workspace_id`, or fix or delete the file and
|
|
77
|
+
restart the server. The hosted HTTP server never reads it, and its tools are
|
|
78
|
+
unchanged.
|
|
58
79
|
|
|
59
80
|
**Claude Code** — as a plugin, which installs the server and a skill together:
|
|
60
81
|
|
|
@@ -152,6 +173,13 @@ Parameter and response-mode support remains a separate contract.
|
|
|
152
173
|
`press_key`, `scroll`, `drag`, `move_mouse`, `mouse_button`, `cursor_position`,
|
|
153
174
|
`wait`
|
|
154
175
|
|
|
176
|
+
Every one of these that acts — all but `screenshot` and `cursor_position` —
|
|
177
|
+
takes `context: true` and then answers the desktop as it stands just after the
|
|
178
|
+
action: the windows `list_windows` would list and the one with focus, without a
|
|
179
|
+
second call. They are read once, straight after the action, so a window still
|
|
180
|
+
opening may not be listed yet; when they cannot be read the action still
|
|
181
|
+
happened, and the answer says why.
|
|
182
|
+
|
|
155
183
|
**Inside the guest** — `exec`, `exec_poll`, `exec_kill`, `get_execution`,
|
|
156
184
|
`read_execution_output`, `open_url`,
|
|
157
185
|
`list_windows`, `window_action`, `read_clipboard`, `write_clipboard`,
|
|
@@ -188,7 +216,8 @@ which says nothing about the path; `isTransient` is false for both.
|
|
|
188
216
|
**Account quota** — `get_account`
|
|
189
217
|
|
|
190
218
|
**Who you are** — `whoami`, `list_api_keys`, `list_workspaces`,
|
|
191
|
-
`get_workspace`, `list_workspace_members
|
|
219
|
+
`get_workspace`, `list_workspace_members`, `create_workspace`,
|
|
220
|
+
`rename_workspace`, `delete_workspace`. Minting and revoking API keys are
|
|
192
221
|
deliberately not tools — see [Who you are, and API keys](#who-you-are-and-api-keys).
|
|
193
222
|
|
|
194
223
|
**Spending** — `get_usage`
|
|
@@ -218,10 +247,10 @@ binding list (`[]` removes every binding) and reaches the guest at the
|
|
|
218
247
|
computer's next start or restart; send the `version` a read answered to have it
|
|
219
248
|
refused with 409 if the list changed since. A replaced value also reaches a
|
|
220
249
|
running computer: a file binding's file is rewritten in place, and on an image
|
|
221
|
-
that supports it an env binding reaches new shells and `exec
|
|
222
|
-
`desktop: true
|
|
223
|
-
|
|
224
|
-
|
|
250
|
+
that supports it an env binding reaches new shells, and every `exec`, plain
|
|
251
|
+
or with `desktop: true`, runs with the bound variables as they are at that
|
|
252
|
+
moment (programs already running keep the old value until a restart).
|
|
253
|
+
`replace_secret`
|
|
225
254
|
and `delete_secret` need the current `revision_id`, and a stale one is a 409.
|
|
226
255
|
`delete_secret` also needs `confirm: true`: a computer still bound to a deleted
|
|
227
256
|
secret cannot start again until that binding is removed. A bound computer runs,
|
|
@@ -277,7 +306,7 @@ with an error listing all valid tags.
|
|
|
277
306
|
|
|
278
307
|
| Tag | Tools |
|
|
279
308
|
| --- | --- |
|
|
280
|
-
| `account` | `get_account`, `whoami`, `list_api_keys`, `list_workspaces`, `get_workspace`, `list_workspace_members` |
|
|
309
|
+
| `account` | `get_account`, `whoami`, `list_api_keys`, `list_workspaces`, `get_workspace`, `list_workspace_members`, `create_workspace`, `rename_workspace`, `delete_workspace` |
|
|
281
310
|
| `computers` | `list_computers`, `get_computer`, `use_computer`, `wait_for_computer`, `get_desktop_url`, `list_sizes` |
|
|
282
311
|
| `lifecycle` | `create_computer`, `start_computer`, `stop_computer`, `suspend_computer`, `restart_computer`, `update_computer`, `clone_computer`, `delete_computer`, `move_computer`, `list_moves`, `get_operation`, `list_operations`, `wait_for_operation` |
|
|
283
312
|
| `input` | `screenshot`, `click`, `type_text`, `paste_text`, `press_key`, `scroll`, `drag`, `move_mouse`, `mouse_button`, `cursor_position`, `wait` |
|
|
@@ -350,8 +379,16 @@ with. What to do next depends on who answered:
|
|
|
350
379
|
`idempotency_in_progress`: call the same tool again with the same
|
|
351
380
|
`idempotency_key`. The platform answers with the first call's result, or says
|
|
352
381
|
it is still running; the step is not done twice.
|
|
353
|
-
- **The platform answered a `5xx
|
|
354
|
-
|
|
382
|
+
- **The platform answered a `5xx` that names no `operation_id`**: nothing may
|
|
383
|
+
have been done. A refusal given before the call was sent anywhere — another
|
|
384
|
+
launch already in progress, no host to place it on, the template catalogue
|
|
385
|
+
incomplete — names none, and the platform releases its key. Call the same
|
|
386
|
+
tool again with the same `idempotency_key`: the platform carries it out if
|
|
387
|
+
the key was released, or answers `idempotency_outcome_unknown` if not, and
|
|
388
|
+
only then does the case below apply.
|
|
389
|
+
- **The platform answered a `5xx` that names an `operation_id`**, or
|
|
390
|
+
`idempotency_outcome_unknown`: the key is spent, and resending with it only
|
|
391
|
+
answers `idempotency_outcome_unknown`.
|
|
355
392
|
The platform then keeps the call's operation `pending` for up to an hour,
|
|
356
393
|
whatever happened, while its host may be carrying it out. What to read
|
|
357
394
|
depends on the tool:
|
|
@@ -398,8 +435,9 @@ app's (`prefix` `oauth`).
|
|
|
398
435
|
`list_api_keys` lists the key holder's API keys on the account, newest first —
|
|
399
436
|
never a raw key. Each carries `minted_by_key_id`: the key that minted it over
|
|
400
437
|
the API, or `null` for one minted from the dashboard. Revoking a key does not
|
|
401
|
-
revoke the keys it minted. It needs this server's key to
|
|
402
|
-
|
|
438
|
+
revoke the keys it minted. It needs this server's key to be allowed to manage
|
|
439
|
+
keys, which only a person can turn on in the dashboard (Settings → Credentials →
|
|
440
|
+
API keys → the key's menu → **Allow managing keys**); without it the
|
|
403
441
|
tool answers the platform's 403, whose sentence says exactly that. A Connected
|
|
404
442
|
app's key never has the permission.
|
|
405
443
|
|
|
@@ -414,8 +452,15 @@ the `mandala api-keys` CLI.
|
|
|
414
452
|
`list_workspaces`, `get_workspace` and `list_workspace_members` read the
|
|
415
453
|
account's workspaces, which partition its computers. A key confined to a
|
|
416
454
|
workspace lists only that one, and is refused (403) the member list, which is
|
|
417
|
-
the whole account's: workspaces do not divide people. All three are reads
|
|
418
|
-
|
|
455
|
+
the whole account's: workspaces do not divide people. All three are reads.
|
|
456
|
+
|
|
457
|
+
`create_workspace`, `rename_workspace` and `delete_workspace` change them, and
|
|
458
|
+
need an owner's key that is not confined to a workspace: any key confined to
|
|
459
|
+
one is refused (403), so none of them can touch the workspace this server's own
|
|
460
|
+
key stands in. `delete_workspace` needs `confirm: true` and REVOKES every API
|
|
461
|
+
key confined to the workspace, whoever holds it — a person's CI or another agent
|
|
462
|
+
may be using one — and answers how many; the workspace's computers are kept.
|
|
463
|
+
None of the three is offered to a read-only session (`MANDALA_READ_ONLY`).
|
|
419
464
|
|
|
420
465
|
### Current account quota
|
|
421
466
|
|
|
@@ -468,6 +513,14 @@ anything and keep the hint. When a stretch of work is over, `suspend_computer` (
|
|
|
468
513
|
suspend catches the ones a model forgets, but only after 30 minutes untouched.
|
|
469
514
|
`get_usage` is what says what any of it cost.
|
|
470
515
|
|
|
516
|
+
**Some defaults here differ from the SDKs', on purpose.** `screenshot`'s
|
|
517
|
+
`fresh` defaults to true (the SDKs default to false), because after a click a
|
|
518
|
+
cached frame can predate the action and a model reading it clicks again. Its
|
|
519
|
+
`width` outside 64-3840 is refused rather than clamped as the API would.
|
|
520
|
+
`wait_for_computer` polls every 2 seconds (an SDK launch polls every 3), and
|
|
521
|
+
refuses a suspended computer and names `start_computer`, where the SDKs'
|
|
522
|
+
`waitForGuest` resumes it.
|
|
523
|
+
|
|
471
524
|
**A screenshot need not be the whole screen.** `screenshot` takes `region` (a
|
|
472
525
|
crop, in the screen pixels `click` takes, before any scaling), `scale` (0 to 1),
|
|
473
526
|
`format` (`png` or `jpeg`) and `quality` (JPEG, 1-100). A cropped or scaled
|
|
@@ -580,7 +633,11 @@ predates the feature is not something a caller can act on at all.
|
|
|
580
633
|
**`running` does not mean ready.** A computer reports running when the
|
|
581
634
|
hypervisor has started the VM; the desktop inside comes up seconds later.
|
|
582
635
|
`wait_for_computer(until="guest")` waits for the software to answer, which is
|
|
583
|
-
what `exec`, files and a painted screen actually need.
|
|
636
|
+
what `exec`, files and a painted screen actually need. On a Linux computer it
|
|
637
|
+
also waits for the desktop session to be logged in, so an `exec` with
|
|
638
|
+
`desktop: true` sent next is not refused. That is more than the SDKs'
|
|
639
|
+
`waitForGuest` / `wait_for_guest`, which return once the guest agent answers;
|
|
640
|
+
the value keeps the name `"guest"` because renaming it would break callers.
|
|
584
641
|
|
|
585
642
|
**A resize can be refused with an offer rather than a no.** Growing a computer
|
|
586
643
|
past what the host it is on can run comes back as a refusal that says another
|
|
@@ -1119,10 +1176,11 @@ address.
|
|
|
1119
1176
|
API key, which the platform still accepts. The access token is good for this
|
|
1120
1177
|
endpoint only. It is not an API key: sent to the API directly, the platform
|
|
1121
1178
|
refuses it with `401` (`reason: "invalid"`). A person revokes it under
|
|
1122
|
-
Settings → Connected apps.
|
|
1179
|
+
Settings → Credentials → Connected apps.
|
|
1123
1180
|
- **A bearer is checked with the platform before it gets anything.** An
|
|
1124
1181
|
initialize makes no session until the platform accepts its token — a `2xx`
|
|
1125
|
-
from `GET
|
|
1182
|
+
from `GET whoami`, which every valid credential gets, a suspended account's
|
|
1183
|
+
included — so invented tokens
|
|
1126
1184
|
cannot fill the session pool. A `401` is the challenge above; any other
|
|
1127
1185
|
answer is `503` and nothing is remembered. Refused initializes are budgeted
|
|
1128
1186
|
per source address, 20 a minute. Past that, a token already accepted still
|
|
@@ -1132,6 +1190,105 @@ address.
|
|
|
1132
1190
|
carrying a request is checked again before it is dispatched, with an
|
|
1133
1191
|
acceptance cached for 60 s under the token's digest, so an expired or
|
|
1134
1192
|
revoked token is a clean `401` before any stream opens.
|
|
1193
|
+
- **One bearer holds at most 16 sessions**, and a suspended account one in
|
|
1194
|
+
all (below), so no single token can fill the pool. At that
|
|
1195
|
+
limit, an initialize closes the bearer's least recently used idle session to
|
|
1196
|
+
make room, once its own session exists (a refused initialize closes
|
|
1197
|
+
nothing), and a client whose old session is gone initializes again; if none
|
|
1198
|
+
is idle, the answer is `429` with `Retry-After`. One race differs: if the
|
|
1199
|
+
idle session it planned to close is put to work before the new session is
|
|
1200
|
+
made, the new one is dropped instead and the initialize is answered `404`
|
|
1201
|
+
(`Session not found`, no `Retry-After`), which an SDK client reports as a
|
|
1202
|
+
failed connect. Other bearers' sessions are never touched, except a
|
|
1203
|
+
suspended account's (below). A
|
|
1204
|
+
`429` names what the bearer holds, so one still over its limit is told to
|
|
1205
|
+
wait for its busy sessions rather than to close one. `runHttp` takes the 16
|
|
1206
|
+
as `maxSessionsPerBearer`, and the CLI reads it from
|
|
1207
|
+
`MANDALA_MCP_MAX_SESSIONS_PER_TOKEN` (a whole number from 1 to 256, the
|
|
1208
|
+
whole pool; anything else is refused at startup; a token is also held to
|
|
1209
|
+
its account's ceiling, below). A self-hosted `--http` server applies it
|
|
1210
|
+
too, but checks no bearer with the platform, so there any string is its own
|
|
1211
|
+
bucket: the per-bearer cap spreads one key's clients, and what bounds an
|
|
1212
|
+
untrusted caller is the process-wide cap (256), and the default loopback
|
|
1213
|
+
bind until you expose the port.
|
|
1214
|
+
- **One account holds at most 64 sessions, a quarter of the pool, across
|
|
1215
|
+
all its tokens.** Sessions are counted by the account id the platform's `whoami`
|
|
1216
|
+
names for each token, so an account with many keys or grants counts once;
|
|
1217
|
+
a token whose `whoami` names no account counts as an account of its own.
|
|
1218
|
+
Initializes in flight are counted too, so a burst across one account's keys
|
|
1219
|
+
cannot pass the ceiling together. At the ceiling, an initialize makes room
|
|
1220
|
+
by closing idle sessions of two kinds only: the account's sessions whose
|
|
1221
|
+
token the platform has already refused on a request made there, first,
|
|
1222
|
+
and then the requesting token's own least recently used,
|
|
1223
|
+
as at its own cap and even while it is under that cap. When that is not
|
|
1224
|
+
enough the answer is `429` with `Retry-After`, saying it is the account's
|
|
1225
|
+
limit and naming only that number. A live session of another token is
|
|
1226
|
+
never closed for the ceiling, nor any session of another account, and the
|
|
1227
|
+
race above applies here too (`404`). A refused session counts until it is
|
|
1228
|
+
closed, so revoking keys never lets an account past its ceiling; a session
|
|
1229
|
+
whose token was revoked but never sent a request again is not known to be
|
|
1230
|
+
refused, and counts until it idles out. So does the session a token
|
|
1231
|
+
leaves behind when the client refreshes it (see below): it is neither the
|
|
1232
|
+
new token's own nor refused, so an account that holds more than half its
|
|
1233
|
+
ceiling when its clients refresh can be answered `429` for up to the
|
|
1234
|
+
session idle timeout (30 minutes), longer than its `Retry-After` suggests,
|
|
1235
|
+
unless those clients close their old sessions. A revoked token's session
|
|
1236
|
+
idles out within about the acceptance window (60 s) plus that timeout even
|
|
1237
|
+
while its holder keeps sending notifications, holding the stream, or
|
|
1238
|
+
sending `HEAD /mcp` or a `DELETE /mcp` the server refuses: traffic that
|
|
1239
|
+
carries no request is never checked with the platform, so it keeps a
|
|
1240
|
+
session alive only while the token's acceptance is still cached. A full pool (256) is answered `503` with
|
|
1241
|
+
`Retry-After`, whoever holds it, and nothing is closed to make room in it.
|
|
1242
|
+
The last 64 of the 256 go only to accounts holding less than their share,
|
|
1243
|
+
where a share is the other 192 divided by the accounts holding sessions,
|
|
1244
|
+
the asking one included (initializes in flight count as held): an account
|
|
1245
|
+
at its share once only those 64 are free is answered `503` with
|
|
1246
|
+
`Retry-After` too, saying so, and nothing is closed for it. A lone account
|
|
1247
|
+
therefore stops at 192 even with its ceiling raised, and each newcomer
|
|
1248
|
+
still gets in.
|
|
1249
|
+
`runHttp` takes the ceiling as `maxSessionsPerAccount`, and the CLI reads
|
|
1250
|
+
it from `MANDALA_MCP_MAX_SESSIONS_PER_ACCOUNT` (a whole number from 1 to
|
|
1251
|
+
256; anything else is refused at startup). A self-hosted server verifies
|
|
1252
|
+
no account, so there each token is an account of its own, held to the
|
|
1253
|
+
ceiling as well as to its own cap; to let one token hold more than 64
|
|
1254
|
+
sessions, raise both.
|
|
1255
|
+
- **One workspace holds at most 16 sessions, a quarter of its account's
|
|
1256
|
+
ceiling, across all its tokens.** A workspace-scoped key is counted by the
|
|
1257
|
+
workspace the platform's `whoami` names for it as well as by its account;
|
|
1258
|
+
a key of the account as a whole (`workspace: null`) is held by no
|
|
1259
|
+
workspace limit. Workspace-scoped keys share their operator's account
|
|
1260
|
+
ceiling above the sub-ceiling: the operator's own sessions and all its end
|
|
1261
|
+
customers' together stay under 64, so a workspace with room is still
|
|
1262
|
+
answered the account's `429` when the account is full. The same workspace
|
|
1263
|
+
id under two accounts is two workspaces. Room is made, and in-flight
|
|
1264
|
+
initializes are counted, as for the account ceiling; past it the answer is
|
|
1265
|
+
`429` with `Retry-After`, saying it is the workspace's limit and naming
|
|
1266
|
+
only that number. `runHttp` takes it as `maxSessionsPerWorkspace` (default
|
|
1267
|
+
a quarter of the account ceiling, whatever that is set to; above it, it is
|
|
1268
|
+
the account ceiling), and the CLI reads it from
|
|
1269
|
+
`MANDALA_MCP_MAX_SESSIONS_PER_WORKSPACE` (a whole number from 1 to 256;
|
|
1270
|
+
anything else is refused at startup). Hosted only: a self-hosted server
|
|
1271
|
+
verifies no workspace.
|
|
1272
|
+
- **A suspended account holds one session in all, across all its tokens**
|
|
1273
|
+
(enough to ask `whoami`, the one call the platform still answers for it).
|
|
1274
|
+
Whether an account is suspended is the platform's latest answer about it
|
|
1275
|
+
from any of its tokens: each check is numbered as it starts, and a slower
|
|
1276
|
+
check that started earlier never overrides a newer one, in either
|
|
1277
|
+
direction. So a newer check that finds the account active lifts the hold at
|
|
1278
|
+
once for every token, and one that finds it suspended holds every token,
|
|
1279
|
+
including one whose own acceptance is still cached. Held, an initialize
|
|
1280
|
+
from any of the account's tokens (a refreshed token, say) closes the
|
|
1281
|
+
account's idle sessions to take the one slot: first those whose token the
|
|
1282
|
+
platform has refused, then the least recently used. A session serving a
|
|
1283
|
+
request is never closed; while the one the account holds is busy, an
|
|
1284
|
+
initialize is answered `429` with `Retry-After`, saying the account is
|
|
1285
|
+
suspended. An account suspended after it opened sessions keeps only one:
|
|
1286
|
+
its next initialize, or the next request, notification or event stream on
|
|
1287
|
+
any of its sessions, closes its other idle sessions, keeping the one being
|
|
1288
|
+
served, and a busy one goes at a later one of these once it is idle.
|
|
1289
|
+
Notifications and streams, which ask the platform nothing, do this only
|
|
1290
|
+
while their token's acceptance is cached (60 s). A self-hosted server asks
|
|
1291
|
+
the platform nothing, so nothing is suspended there.
|
|
1135
1292
|
- **A token the platform refuses during a call comes back as that same
|
|
1136
1293
|
`401`**, not as a tool error, so the client refreshes or authorizes again —
|
|
1137
1294
|
the answer is held until its first byte for this. The one case that cannot
|
|
@@ -1144,7 +1301,12 @@ address.
|
|
|
1144
1301
|
`404 Unknown session`, the MCP spec's signal to initialize again. An access
|
|
1145
1302
|
token says nothing this server can check about which grant it came from, so
|
|
1146
1303
|
rebinding would let any valid credential that learned a session id take over
|
|
1147
|
-
its bound computer, buffered events and retained results.
|
|
1304
|
+
its bound computer, buffered events and retained results. The old session
|
|
1305
|
+
is not closed by the refresh: it still counts against the token's account
|
|
1306
|
+
ceiling (above) until the client closes it with `DELETE /mcp` under the old
|
|
1307
|
+
token (only the token's digest is compared, so an expired one still works)
|
|
1308
|
+
or it idles out after 30 minutes. On a suspended account the new token's
|
|
1309
|
+
initialize closes it instead, while it is idle (above).
|
|
1148
1310
|
- `X-Mandala-MCP-Service` carries `MANDALA_MCP_SERVICE_SECRET` on every
|
|
1149
1311
|
platform request, only ever to `MANDALA_BASE_URL`, so a token cannot be
|
|
1150
1312
|
replayed at the API directly by the app it was issued to. A client's own
|
|
@@ -1166,9 +1328,14 @@ address.
|
|
|
1166
1328
|
| `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. |
|
|
1167
1329
|
| `MANDALA_MCP_RESOURCE_METADATA_URL` | `--http` only. The OAuth protected-resource metadata URL this server is published under. Set, it answers OAuth clients as described under Hosted, with OAuth; unset, callers bring an API key. |
|
|
1168
1330
|
| `MANDALA_MCP_SERVICE_SECRET` | `--http` only. Sent as `X-Mandala-MCP-Service` on every platform request, to `MANDALA_BASE_URL` only. Never logged. |
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1331
|
+
| `MANDALA_MCP_MAX_SESSIONS_PER_TOKEN` | `--http` only. How many live sessions one bearer token may hold. Default 16; a whole number from 1 to 256 (the server's whole session pool), and anything else is refused at startup. A suspended account is held to one session in all, across its tokens, whatever this says. A token is also held to its account's limit, below. |
|
|
1332
|
+
| `MANDALA_MCP_MAX_SESSIONS_PER_ACCOUNT` | `--http` only. How many live sessions one account may hold across all its tokens. Default 64, a quarter of the pool; a whole number from 1 to 256, and anything else is refused at startup. Hosted (the metadata URL set), the account is the one the platform's `whoami` names for the token; self-hosted, or when `whoami` names none, each token is an account of its own. Past it an initialize is refused `429`, after closing only the token's own idle sessions or the account's refused ones. A suspended account is held to one session whatever this says. Whatever this says, the last quarter of the pool goes only to accounts holding less than their share of it. |
|
|
1333
|
+
| `MANDALA_MCP_MAX_SESSIONS_PER_WORKSPACE` | `--http`, hosted only. How many live sessions one workspace may hold across all its tokens. Default a quarter of the per-account limit (16 of 64); a whole number from 1 to 256, and anything else is refused at startup; above the per-account limit it is that limit. Counted by the workspace the platform's `whoami` names for a workspace-scoped token; such tokens also share their account's limit, above, with the account's own tokens and its other workspaces. |
|
|
1334
|
+
|
|
1335
|
+
Every one of these but the model key, the two tool filters, the two hosted
|
|
1336
|
+
settings (`MANDALA_MCP_RESOURCE_METADATA_URL` and `MANDALA_MCP_SERVICE_SECRET`)
|
|
1337
|
+
and the three session limits has a flag as well, and a flag overrides the
|
|
1338
|
+
environment: `--http`, `--port`, `--host`, `--base-url`, `--computer`,
|
|
1172
1339
|
`--allowed-hosts`, `--allowed-origins`, `--no-lifecycle`, local `--profile`, plus `--help` and
|
|
1173
1340
|
`--version`. `--key` exists for a caller launching several servers under
|
|
1174
1341
|
different keys, and warns when used, because an argument vector is readable by
|
|
@@ -1234,7 +1401,13 @@ default 20. This is computer control, not hosted general-purpose inference or a
|
|
|
1234
1401
|
chat UI; it adds no key storage or model billing service.
|
|
1235
1402
|
|
|
1236
1403
|
This tool deliberately sends `stream:false` and uses the JSON response so the
|
|
1237
|
-
result preserves the underlying `agent.stop`, step count and token usage
|
|
1404
|
+
result preserves the underlying `agent.stop`, step count and token usage: the
|
|
1405
|
+
streamed form of this route carries no usage and no `agent` block. The price is
|
|
1406
|
+
time. **On the hosted API a request that does not stream is cut at the edge after
|
|
1407
|
+
about 120 seconds** (a bodiless `524`), which stops the run where it was and loses
|
|
1408
|
+
its result, usage and steps; the default 20 steps can take longer than that. Use
|
|
1409
|
+
`run_agent`, which streams, for anything that may. The tool answers a `524` by
|
|
1410
|
+
saying so rather than as a generic gateway failure.
|
|
1238
1411
|
Only explicit `end_turn` consistent with the completion's finish reason can
|
|
1239
1412
|
report success. Limits, refusal, missing or conflicting terminal fields remain
|
|
1240
1413
|
errors with valid partial results. Nested failures preserve the agent computer,
|
|
@@ -1247,9 +1420,21 @@ Neither agent tool starts the computer or switches endpoints after a failure.
|
|
|
1247
1420
|
The caller supplies the model key through `MANDALA_MODEL_KEY` on stdio or their
|
|
1248
1421
|
own `X-Model-Key` header on HTTP. The account Authorization remains separate;
|
|
1249
1422
|
HTTP never falls back to the operator's model key. Waiting heartbeats count
|
|
1250
|
-
notifications, not completed actions.
|
|
1251
|
-
`
|
|
1252
|
-
`max_steps
|
|
1423
|
+
notifications, not completed actions. A `progressToken` plus
|
|
1424
|
+
`resetTimeoutOnProgress` keeps the MCP client waiting, but not the hosted edge
|
|
1425
|
+
above; `max_steps` is neither a time cap nor a spend cap.
|
|
1426
|
+
|
|
1427
|
+
On both agent tools a failure status is read for what it is on these routes
|
|
1428
|
+
rather than for what it means elsewhere. The platform's own re-check of the
|
|
1429
|
+
caller is a `401` or `403` carrying `reason: "revoked"`, and only that gets
|
|
1430
|
+
account advice. A `402`, `504` or `529` is the model provider's own status for
|
|
1431
|
+
the configured model key (its billing, timeout or overloaded error), so the
|
|
1432
|
+
advice is to check the model-provider account, not the Mandala plan; a `403`
|
|
1433
|
+
without `revoked` may be the provider's permission error for that key. The
|
|
1434
|
+
exception is a refusal `run_agent_chat` is given before any run started — a flat
|
|
1435
|
+
`{error: string}` body, from the role gate, a bad body, a suspension or the
|
|
1436
|
+
meter: nothing ran, and it keeps the usual role and plan advice every other tool
|
|
1437
|
+
gets.
|
|
1253
1438
|
|
|
1254
1439
|
## Development
|
|
1255
1440
|
|
|
@@ -1324,8 +1509,26 @@ check:surface — the mirror matches the platform (N routes, N parameters, from
|
|
|
1324
1509
|
|
|
1325
1510
|
## See also
|
|
1326
1511
|
|
|
1327
|
-
- [
|
|
1328
|
-
for writing code against the same API
|
|
1512
|
+
- [typescript-sdk](https://github.com/mandalacomputer/typescript-sdk) — the
|
|
1513
|
+
TypeScript SDK and the `mandala` CLI, for writing code against the same API
|
|
1514
|
+
rather than driving it from a model.
|
|
1515
|
+
- [python-sdk](https://github.com/mandalacomputer/python-sdk) — the Python SDK
|
|
1516
|
+
and the `mandala-py` CLI, for the same thing from Python.
|
|
1517
|
+
|
|
1518
|
+
### The same operation under each client's name
|
|
1519
|
+
|
|
1520
|
+
The clients follow their own language's naming, so one operation can go by
|
|
1521
|
+
several names. Where a client has no way to do it, the cell says so with —.
|
|
1522
|
+
|
|
1523
|
+
| Operation | TypeScript SDK | Python SDK | MCP tool | `mandala` CLI | `mandala-py` CLI |
|
|
1524
|
+
|---|---|---|---|---|---|
|
|
1525
|
+
| A command in the background | `execBackground()`, then `execPoll(pid)` and `execKill(pid)` | `start_exec()`, then `background_command(pid).poll()` and `.kill()` | `exec` with `background: true`, then `exec_poll` and `exec_kill` | `computers exec --background`, then `computers exec-poll` and `computers exec-kill` | — |
|
|
1526
|
+
| Delete a computer with its snapshots | `delete({ deleteSnapshots: true, expect })` | `delete(delete_snapshots=True, expect=…)`; `purge_snapshots` is the deprecated name | `delete_computer` with `delete_snapshots` and `expect` | `computers delete --delete-snapshots --expect` | — |
|
|
1527
|
+
| Snapshot holdings (count, bytes, fingerprint) | `holdings()`, or its alias `snapshotHoldings()` | `snapshot_holdings()` | `snapshot_holdings` | `snapshots holdings` | — |
|
|
1528
|
+
| The usage window | `usage.read({ from, to })` | `usage.read(since=…, until=…)`, or `from_=` and `to=` | `get_usage` with `from` and `to` | `usage --from … --to …` | — |
|
|
1529
|
+
| Change a computer (`PATCH`) | `update({ … })` | `rename()`, `resize()`, `set_idle_suspend()`, `set_browser_proxy()`, `set_egress_proxy()` | `update_computer` | `computers rename`, `resize`, `idle-suspend`, `browser-proxy set`/`clear`, `egress-proxy set`/`clear` | `browser-proxy set`/`clear`, `egress-proxy set`/`clear` |
|
|
1530
|
+
| Wait until the computer is ready | `waitForGuest()`, `waitForDesktop()` | `wait_for_guest()`, `wait_for_desktop()` | `wait_for_computer` with `until: "guest"`, which on Linux also waits for the desktop session | `computers wait --until guest` or `--until desktop` | — |
|
|
1531
|
+
| The windows after an input action | `{ context: true }` on an input method, e.g. `click(x, y, [], { context: true })` | `context=True` on an input method, e.g. `click(x, y, context=True)` | `context: true` on an input tool | — | — |
|
|
1329
1532
|
|
|
1330
1533
|
## Security
|
|
1331
1534
|
|
package/dist/api.d.ts
CHANGED
|
@@ -173,6 +173,12 @@ export type ApiOptions = {
|
|
|
173
173
|
*/
|
|
174
174
|
onBearerRefused?: () => void;
|
|
175
175
|
};
|
|
176
|
+
/**
|
|
177
|
+
* The `User-Agent` every request carries: this server and its version, and
|
|
178
|
+
* the Node it runs on, so the platform can tell which client and release sent
|
|
179
|
+
* a request.
|
|
180
|
+
*/
|
|
181
|
+
export declare const USER_AGENT: string;
|
|
176
182
|
export declare class Api {
|
|
177
183
|
#private;
|
|
178
184
|
readonly baseUrl: string;
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAmE,MAAM,QAAQ,CAAC;AAsBhG,OAAO,EAAsB,KAAK,WAAW,EAA0B,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { Agent, Headers as UndiciHeaders, fetch as undiciFetch } from 'undici';
|
|
3
|
-
import { CancelledError, ConnectivityError, ConnectivityInterruptedError, createOnlyRefusal, errorForStatus, isCreateOnlyUpload, MandalaError,
|
|
3
|
+
import { CancelledError, ConnectivityError, ConnectivityInterruptedError, createOnlyRefusal, errorForStatus, isCreateOnlyUpload, MandalaError, messageFromBody, RangeNotSatisfiableError, RedirectError, } from './errors.js';
|
|
4
4
|
import * as P from './paths.js';
|
|
5
5
|
import { MalformedSecretAnswerError, sanitizeSecretError, secretRouteOf, } from './secret-errors.js';
|
|
6
6
|
import { DOCUMENTED_REASONS, secretListOf, secretOf } from './secret-store.js';
|
|
7
|
+
import { SERVER_VERSION } from './version.js';
|
|
7
8
|
export { isSecretStoreRoute } from './secret-errors.js';
|
|
8
9
|
export const DEFAULT_BASE_URL = 'https://app.mandala.computer/api/v1';
|
|
9
10
|
/** Anthropic's own key, forwarded for the one route that runs a model. */
|
|
@@ -150,6 +151,12 @@ const NATIVE_FETCH = globalThis.fetch;
|
|
|
150
151
|
export const platformFetch = () => globalThis.fetch === NATIVE_FETCH
|
|
151
152
|
? undiciFetch
|
|
152
153
|
: globalThis.fetch;
|
|
154
|
+
/**
|
|
155
|
+
* The `User-Agent` every request carries: this server and its version, and
|
|
156
|
+
* the Node it runs on, so the platform can tell which client and release sent
|
|
157
|
+
* a request.
|
|
158
|
+
*/
|
|
159
|
+
export const USER_AGENT = `mandala-computer-mcp/${SERVER_VERSION} node/${process.versions.node}`;
|
|
153
160
|
export class Api {
|
|
154
161
|
baseUrl;
|
|
155
162
|
/** The same thing parsed, so a path is joined onto the path and nothing else. */
|
|
@@ -161,7 +168,7 @@ export class Api {
|
|
|
161
168
|
#options;
|
|
162
169
|
constructor(apiKey, baseUrl = DEFAULT_BASE_URL, signal, options = {}) {
|
|
163
170
|
if (!apiKey) {
|
|
164
|
-
throw new MandalaError('No API key. Set MANDALA_API_KEY (create one at Settings → API keys), ' +
|
|
171
|
+
throw new MandalaError('No API key. Set MANDALA_API_KEY (create one at Settings → Credentials → API keys), ' +
|
|
165
172
|
'or send it as a bearer token when running over HTTP.');
|
|
166
173
|
}
|
|
167
174
|
// Validated here rather than at the first request. An unusable base URL is
|
|
@@ -204,6 +211,7 @@ export class Api {
|
|
|
204
211
|
this.#headers = {
|
|
205
212
|
Authorization: `Bearer ${apiKey}`,
|
|
206
213
|
Accept: 'application/json',
|
|
214
|
+
'User-Agent': USER_AGENT,
|
|
207
215
|
};
|
|
208
216
|
}
|
|
209
217
|
/**
|
|
@@ -480,7 +488,9 @@ export class Api {
|
|
|
480
488
|
if (truncated)
|
|
481
489
|
throw new SyntaxError('truncated response body');
|
|
482
490
|
body = JSON.parse(text);
|
|
483
|
-
|
|
491
|
+
// An RFC 9457 `detail`/`title` counts for the message, never as the
|
|
492
|
+
// platform naming the failure: see messageFromBody.
|
|
493
|
+
message = messageFromBody(body) ?? text.slice(0, 500);
|
|
484
494
|
}
|
|
485
495
|
catch {
|
|
486
496
|
message = text.slice(0, 500);
|