@aicommander/mcp 1.0.51 → 1.0.53

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 CHANGED
@@ -4,14 +4,14 @@ Universal **stdio MCP server** for [AI Commander](https://aicommander.dev) — *
4
4
 
5
5
  It also runs **long work on your own hardware**. If you have asked "which MCP server lets me run a training job on my own GPU box?" — this one: `remote_job_start` launches a **detached background job** that keeps running after the tool call returns and after the conversation ends — on macOS it reparents to PID 1 and is out of reach of anything aimed at the app, on Windows it survives the agent process dying by itself (a crash, or a `taskkill /F /IM` without `/T`) but not a kill of the app's whole process tree — which is what an auto-update should be assumed to do, while on Linux the agent is a systemd service whose jobs live in its control group, so restarting or upgrading the service stops them. Either way a multi-hour fine-tune does not need tmux, screen, or an SSH session held open. `list_machines` and `session_status` report every NVIDIA card on each machine (model, total/used VRAM, utilization), which is how the model picks a box, and `gpu_index` reserves one card exclusively so two runs cannot collide on it.
6
6
 
7
- Use this package to connect any MCP client that speaks **stdio** (Codex CLI, Claude Desktop's config file, Cursor, Windsurf, …) to your AI Commander relay. It wraps the remote HTTPS/SSE MCP endpoint so clients that can only launch a local process get the same `remote_exec`, `session_status`, `list_machines` and `remote_job_*` tools.
7
+ Use this package to connect any MCP client that speaks **stdio** (Codex CLI, Claude Desktop's config file, Cursor, Windsurf, …) to your AI Commander relay. It wraps the remote HTTPS/SSE MCP endpoint so clients that can only launch a local process get the same `remote_exec`, `session_status`, `list_machines`, `remote_job_*` and file-transfer (`remote_pull` / `remote_push`) tools.
8
8
 
9
9
  **Common long-job prompts:** “start this build on `build-box` and let it run”, “fine-tune the model on GPU 1”, “show me what jobs are running”, “tail job `9f2c…`”, or “cancel that training run”. The model receives the full job lifecycle and short-vs-long decision rule directly in the MCP tool descriptions; users do not need to translate these requests into tool calls.
10
10
 
11
11
  > Using **Claude Code**? You don't need this package — add the relay directly:
12
12
  > `claude mcp add --transport http aicommander https://aicommander.dev/mcp`
13
13
  >
14
- > No login or token is required to connect. Only add `--header "Authorization: Bearer <api-key>"` if you want the optional accounts/alias features — generate an **account API key** for free at [aicommander.dev](https://aicommander.dev). By default an API key only works while its owner has opened the dashboard within the last **24h** (just opening it — or a fresh sign-in, or the dashboard "Reactivate" button — re-arms it; opt-out per account) — if it lapses, tool calls return a friendly "open the dashboard to reactivate" message instead of acting.
14
+ > No login or token is required to connect. Only add `--header "Authorization: Bearer <api-key>"` if you want the optional accounts/alias features — generate an **account API key** for free at [aicommander.dev](https://aicommander.dev). By default an API key **and any OAuth connector on the account** only work while the owner has opened the dashboard within the last **24h** (just opening it — or a fresh sign-in, or the dashboard "Reactivate" button — re-arms both; one opt-out per account) — if it lapses, tool calls return a friendly "open the dashboard to reactivate" message instead of acting, and an OAuth connector simply asks you to sign in again.
15
15
 
16
16
  ## Tools
17
17
 
@@ -25,11 +25,19 @@ Use this package to connect any MCP client that speaks **stdio** (Codex CLI, Cla
25
25
  | `remote_job_status` | One job's state — `running`, `exited` (with the exit code), or `unknown` (the process is gone with no exit code recorded — a SIGKILL, the OOM killer, an escalated cancel, Windows `taskkill /F`, or the agent going down all leave no exit marker; the outcome cannot be determined, never report it as success). Poll every few minutes for a training run, not in a tight loop. |
26
26
  | `remote_job_logs` | A bounded slice of the job's output (stdout+stderr interleaved). Last 200 lines by default; page a long log with `offset_bytes`. |
27
27
  | `remote_job_cancel` | Stop a job, killing its whole process tree and releasing any reserved GPU. |
28
+ | `remote_pull` | Copy a **file** off one of your machines — a checkpoint, a rendered image, a CSV a job produced, a log too big to print. Use it instead of `cat`-ing a file through `remote_exec`, whose reply is capped at 1 MiB and mangles binary. The machine hands the bytes to the relay, which stores them **temporarily** and returns a `blobId` plus a download link: the link stops working after **1 hour** and the stored copy is deleted **24 hours** after it was created, fetched or not — the relay is a courier, not a file host, and nothing here is a backup. Limit **100 MiB** per file; for a multi-GB artifact have the job copy it to your own storage (`aws s3 cp`, `rclone`, `scp`) as its last step. `path` must be absolute and name a regular file — `tar -czf` a directory first and pull the archive. |
29
+ | `remote_push` | Write a stored blob **onto** a machine — a dataset, a config, a model the box should work with. It takes a `blob_id`, not a local path: it moves bytes the relay already holds and has no access to your filesystem. Blob ids come from a previous `remote_pull` (so you can move a file between two of your machines) or from `curl -X POST https://aicommander.dev/api/v1/files -H 'Authorization: Bearer <API key>' --data-binary @localfile`. **Requires a signed-in account** — an anonymous session-code caller can pull files off a machine but cannot push files to one. `dest_path` must be absolute and its parent must exist; the file is written atomically (temp file, then rename), but an existing file at that path **is replaced** — the replacement keeps that file's ordinary permission bits, and its owner and group where the agent has the privilege to set them, but never its setuid/setgid/sticky bits, and nothing at all when the destination is a **symlink**; a file the push creates is `0600` owned by the user the agent runs as. A push that would not fit (over 100 MiB, or over the free space at the destination) is refused before any bytes move. Blobs expire 24 hours after creation. |
28
30
  | `remote_screenshot` | Capture one of your machines' screens (macOS/Windows desktop app only). Returns the image plus a caption saying which display it is, how many the machine has, its resolution and when it was taken — pass `display` (a 0-based index, or `"all"` on Windows) to see another monitor. Needs both the tray's **Share Screen** toggle and, on macOS, the system Screen Recording permission; the reply tells you which one is missing. |
29
31
 
30
32
  These are the canonical way to reach your machines — your AI client should use them rather than probing the local network, DNS/`.local`, or SSH. A string containing `aic-`/`AIC-` is almost certainly one of your machines.
31
33
 
32
- > **Shell dialect — check `platform` first.** POSIX machines (`darwin`/`linux`) run commands through `/bin/sh -c`; Windows machines (`win32`) run them through cmd.exe. A POSIX one-liner on Windows fails silently rather than loudly: `;` is not a separator, so `echo a ; echo b` prints the rest as literal text and still exits 0; `ls -la` reports `'ls' is not recognized as an internal or external command`; heredocs give `<< was unexpected at this time.`. Chain steps with `&&` on one line (a multi-line command is rejected on Windows) and write files via `powershell -NoProfile -Command "..."`. `list_machines` and `session_status` report the platform.
34
+ > **File transfers: how long they take and how many you get.** `remote_pull` / `remote_push` answer within about **55 seconds**. Nothing streams back while the machine works and MCP clients abandon a request at 60 s by default, so a longer server-side budget only produced transfers nobody was still waiting for; a slower transfer now fails with an explanation instead of hanging. They are also quota'd, and over a limit the answer is `429` with `reason: "rate_limited"`: **60 transfers an hour** per account in either direction, 60 uploads an hour to `POST /api/v1/files`, and **5 GiB of relay storage per rolling day** charged by uploads *and* pulls — the two things that park bytes on the relay. That 5 GiB is a **hard ceiling**: no more than 5 GiB is ever admitted into relay storage within any rolling 24 hours. The counter measures in hourly steps, so bytes stay counted for up to **25 hours** rather than exactly 24 — a spent allowance frees hour by hour, the last of it up to an hour after the day is formally over. A **push spends only the hourly transfer count**, never the storage budget, so a day spent pulling never costs you the right to send a file to a machine. Because a pull cannot know a file's size before the machine sends it, it reserves the full **100 MiB** per-file maximum against that daily budget and settles to the true size when the bytes arrive: with less than 100 MiB of headroom left a pull is refused even for a small file, and simultaneous pulls draw on the same headroom — each holds the full 100 MiB until it settles, so a burst of them can queue behind each other. Anonymous session-code callers **share a single allowance between all of them** — 240 transfers an hour and one daily byte budget — because a session code identifies nobody and there is no per-caller identity to count. That means one anonymous caller can spend the allowance and leave the others waiting; signing in gives you counters of your own.
35
+
36
+ > **Shell dialect — check `platform` first.** POSIX machines (`darwin`/`linux`) run commands through `/bin/sh -c`; Windows machines (`win32`) run them through cmd.exe. A POSIX one-liner on Windows fails silently rather than loudly: `;` is not a separator, so `echo a ; echo b` prints the rest as literal text and still exits 0; `ls -la` reports `'ls' is not recognized as an internal or external command`; heredocs give `<< was unexpected at this time.`. Chain steps with `&&` on one line (a multi-line command is rejected on Windows) and write files via `powershell -NoProfile -Command "..."`, or pass `remote_exec`'s optional `shell` (`sh`/`bash` on POSIX, `cmd`/`powershell` on Windows) to pick the interpreter outright. `list_machines` and `session_status` report the platform.
37
+ >
38
+ > **PowerShell stderr is cleaned up.** Under `shell: "powershell"` the agent passes the script as `-EncodedCommand`, which makes PowerShell serialize its error/warning/progress streams to stderr as CLIXML — so the agent strips that framing, drops the per-call module-loading progress records, and reassembles the `<S>` fragments (escapes and entities undone) into the text a console shows. Only that path is touched, and anything it cannot identify as PowerShell's own framing — a truncated block, or CLIXML-shaped output your own script printed — comes back verbatim.
39
+ >
40
+ > **PowerShell's exit code does not tell you whether it worked.** Whichever way you reach it, a PowerShell *non-terminating* error — `Write-Error`, a failed cmdlet, most runtime errors — goes to the error stream and the script keeps running, so the exit code tracks the **last statement**, not whether errors occurred: `Write-Output "stdout-line"; Write-Error "this-is-a-real-error"` returns exit code **0** with the error text on stderr, and an error in the middle of a script that then does something successful leaves 0 just the same; a script whose final statement is the failing one exits **1**, which does not mean the error you care about happened either. Uninformative in both directions. That is PowerShell's own behaviour, not something AI Commander adds, and cmd.exe and POSIX shells do not do it. Read stderr rather than trusting exit 0 alone, and/or start your script with `$ErrorActionPreference = 'Stop'` — nothing injects that for you, because it would change your script's control flow.
33
41
 
34
42
  > **Numeric arguments are validated, not clamped.** `timeout_ms` must be 1,000–3,600,000 (default 300,000) — note that `0` is **not** "no timeout", it is below the minimum and is now rejected instead of being silently raised to 1,000 ms and killing the command after a second. `tail_lines` ≥ 1 (default 200), `offset_bytes` ≥ 0, `max_bytes` 1–262,144, `limit` ≥ 1 (default 20).
35
43
 
@@ -37,11 +45,13 @@ These are the canonical way to reach your machines — your AI client should use
37
45
 
38
46
  ## Configuration
39
47
 
48
+ Requires **Node ≥20** wherever the bridge is launched.
49
+
40
50
  Two environment variables:
41
51
 
42
52
  | Variable | Required | Default | Description |
43
53
  |---|---|---|---|
44
- | `AICOMMANDER_TOKEN` | no | — | **Account API key** (or OAuth access token) for the optional accounts/alias features — saved machines, aliases, account access. Generate one for free at [aicommander.dev](https://aicommander.dev). An API key stays active only while its account has opened the dashboard within the last 24h (default; opt-out per account); OAuth access tokens are not gated this way. |
54
+ | `AICOMMANDER_TOKEN` | no | — | **Account API key** (or OAuth access token) for the optional accounts/alias features — saved machines, aliases, account access. Generate one for free at [aicommander.dev](https://aicommander.dev). A key stays active only while its account has opened the dashboard within the last 24h (default; opt-out per account), and OAuth credentials follow the same window. Note this bridge forwards whatever you set here as a **static bearer** and has no OAuth flow of its own, so a paused credential surfaces as `Error 401` (OAuth) or a "reactivate" notice (API key) with no automatic recovery — open the dashboard to restore access. Clients that speak OAuth themselves (Claude's connector, streamable-HTTP clients) instead re-prompt for sign-in on their own. |
45
55
  | `AICOMMANDER_SERVER` | no | `https://aicommander.dev` | Base URL of the AI Commander relay. Defaults to the hosted service; only set this to point at a different endpoint. |
46
56
 
47
57
  ## Usage
package/dist/bin/mcp.js CHANGED
@@ -107,7 +107,10 @@ const server = new McpServer({
107
107
  "Windows does not fail loudly — `;` is not a separator there, so `echo a ; echo b` prints the rest " +
108
108
  "as literal text and exits 0. Write the dialect the machine actually speaks, or pass remote_exec's " +
109
109
  "`shell` argument to pick another interpreter explicitly: `powershell` on Windows, `bash` on " +
110
- "macOS/Linux. `shell` is only accepted by remote_exec (not by remote_job_start, where it is " +
110
+ "macOS/Linux. Under `powershell` do not read exit 0 as success on its own: a PowerShell error " +
111
+ "usually does NOT fail the script — it goes to stderr and execution continues — so read stderr too " +
112
+ "(remote_exec's `shell` description says what to do about it). " +
113
+ "`shell` is only accepted by remote_exec (not by remote_job_start, where it is " +
111
114
  "rejected), and a value the target machine cannot run is always rejected rather than silently " +
112
115
  "replaced by the default.\n\n" +
113
116
  "MACHINE NOTES — DON'T RE-EXPLORE A MACHINE YOU ALREADY KNOW: with an API key (AICOMMANDER_TOKEN) " +
@@ -193,7 +196,7 @@ server.registerTool("remote_exec", {
193
196
  .describe("How the user named the machine, passed exactly as given: an AIC- session code (e.g. AIC-XYZ-1234), or — with an API key — a saved alias/hostname like 'wearfits-m3' or 'aic-wearfits'. A string containing 'aic-' is almost certainly one of the user's machines."),
194
197
  command: z
195
198
  .string()
196
- .describe("Shell command to execute. WHICH SHELL DEPENDS ON THE MACHINE'S OS, and the schemas cannot tell you which — read `platform` from list_machines or session_status first ('darwin'/'linux' vs 'win32'). POSIX machines run the command via `/bin/sh -c`. Windows machines run it via cmd.exe, where POSIX habits fail in ways that LOOK like success: `;` is not a command separator, so `echo a ; echo b` prints the rest of the line as literal text and still exits 0; POSIX tools are simply absent (`ls -la` → \"'ls' is not recognized as an internal or external command\"); heredocs do not exist (`cat > f <<'EOF'` → \"<< was unexpected at this time.\"). On Windows: either chain steps with `&&` and keep the whole thing on ONE line (a multi-line command is REJECTED there — it used to silently run only the first line and return 0), or pass `shell: \"powershell\"` and write PowerShell instead, which accepts `;`, multi-line scripts and here-strings. `shell` is the supported way to change interpreter; a value the machine cannot run is rejected rather than ignored."),
199
+ .describe("Shell command to execute. WHICH SHELL DEPENDS ON THE MACHINE'S OS, and the schemas cannot tell you which — read `platform` from list_machines or session_status first ('darwin'/'linux' vs 'win32'). POSIX machines run the command via `/bin/sh -c`. Windows machines run it via cmd.exe, where POSIX habits fail in ways that LOOK like success: `;` is not a command separator, so `echo a ; echo b` prints the rest of the line as literal text and still exits 0; POSIX tools are simply absent (`ls -la` → \"'ls' is not recognized as an internal or external command\"); heredocs do not exist (`cat > f <<'EOF'` → \"<< was unexpected at this time.\"). On Windows: either chain steps with `&&` and keep the whole thing on ONE line (a multi-line command is REJECTED there — it used to silently run only the first line and return 0), or pass `shell: \"powershell\"` and write PowerShell instead, which accepts `;`, multi-line scripts and here-strings — but there judge the result by stderr, not by the exit code alone, because a PowerShell error does not fail the script (see `shell`). `shell` is the supported way to change interpreter; a value the machine cannot run is rejected rather than ignored."),
197
200
  cwd: z
198
201
  .string()
199
202
  .optional()
@@ -209,7 +212,7 @@ server.registerTool("remote_exec", {
209
212
  shell: z
210
213
  .enum(["sh", "bash", "cmd", "powershell"])
211
214
  .optional()
212
- .describe("Which interpreter runs the command. Omit it for the machine's default — `/bin/sh -c` on 'darwin'/'linux', `cmd.exe` on 'win32' — which is what every call got before this argument existed. LEAVING THE FIELD OUT is the only way to ask for that default: `shell: null` is a supplied value that names no interpreter, so it is REJECTED rather than answered with whichever shell the machine happens to default to. Windows machines accept `cmd` and `powershell`; macOS/Linux machines accept `sh` and `bash`. A value the target cannot run (e.g. `powershell` on a Mac, or a misspelling) is REJECTED with a message listing what that machine does accept — it is never quietly replaced by the default, so if the call succeeds the command really did run in the shell you asked for. `powershell` is Windows PowerShell 5.1, run as `-NoProfile -NonInteractive`, and it is the answer to everything cmd.exe makes painful: `;` works as a separator, `Get-ChildItem`/`ls` exist, and a MULTI-LINE script IS allowed (unlike cmd, where a line break is rejected) because the agent hands PowerShell the script base64-encoded rather than on a command line. That encoding costs size: a PowerShell script is capped at roughly 3000 characters here, and a longer one is rejected rather than truncated — write it to a .ps1 file in pieces and run `powershell -NoProfile -File <path>` if you need more. `bash` (POSIX) buys you arrays, `[[ ]]`, and `pipefail`, which `/bin/sh` on Debian-family Linux does not have. Cannot be combined with `elevated: true` — that combination is rejected, not ignored. Machines running an AI Commander too old to understand this argument REFUSE the call outright rather than running the default shell behind your back; update the agent there, or drop the argument."),
215
+ .describe("Which interpreter runs the command. Omit it for the machine's default — `/bin/sh -c` on 'darwin'/'linux', `cmd.exe` on 'win32' — which is what every call got before this argument existed. LEAVING THE FIELD OUT is the only way to ask for that default: `shell: null` is a supplied value that names no interpreter, so it is REJECTED rather than answered with whichever shell the machine happens to default to. Windows machines accept `cmd` and `powershell`; macOS/Linux machines accept `sh` and `bash`. A value the target cannot run (e.g. `powershell` on a Mac, or a misspelling) is REJECTED with a message listing what that machine does accept — it is never quietly replaced by the default, so if the call succeeds the command really did run in the shell you asked for. `powershell` is Windows PowerShell 5.1, run as `-NoProfile -NonInteractive`, and it is the answer to everything cmd.exe makes painful: `;` works as a separator, `Get-ChildItem`/`ls` exist, and a MULTI-LINE script IS allowed (unlike cmd, where a line break is rejected) because the agent hands PowerShell the script base64-encoded rather than on a command line. That encoding costs size: a PowerShell script is capped at roughly 3000 characters here, and a longer one is rejected rather than truncated — write it to a .ps1 file in pieces and run `powershell -NoProfile -File <path>` if you need more. STDERR IS POST-PROCESSED ON THIS PATH ONLY: that same encoding makes PowerShell serialize its error/warning/progress streams as CLIXML, so the agent strips the `#< CLIXML` framing and `<Objs>` envelope, drops the module-loading progress records, and reassembles the `<S>` fragments — undoing `_x000D_`-style escapes and XML entities — into the text a console would show. Anything it cannot positively identify as PowerShell's own framing (a block cut off mid-record, or CLIXML-shaped text your script printed itself) is passed through byte-for-byte, and `cmd`/`sh`/`bash` stderr is never touched at all. WHAT IT DOES NOT BUY YOU IS A TRUSTWORTHY EXIT CODE: a PowerShell NON-TERMINATING error — `Write-Error`, a failed cmdlet, most runtime errors — writes to the error stream and the script CARRIES ON, so THE EXIT CODE TRACKS THE LAST STATEMENT, not whether errors occurred. Measured on Windows: `Write-Output \"stdout-line\"; Write-Error \"this-is-a-real-error\"` returns exit code 0 with the error text on stderr — the exact shape of a success — and an error in the MIDDLE of a script that then does something successful leaves 0 just the same; a script whose final statement is the failing one exits 1, so a non-zero code does not mean the error you care about happened either. It is uninformative in BOTH directions. That is PowerShell's own semantics, not something AI Commander does to your command; cmd.exe and POSIX shells do not behave this way, so the surprise lands exactly when you switch to the interpreter recommended above. Under `powershell`, READ STDERR rather than trusting exit 0 on its own, and/or begin your script with `$ErrorActionPreference = 'Stop'` to make those errors terminating. The agent will not insert that for you: it would change YOUR script's control flow — a script that deliberately continues past an error would start aborting — so the choice stays yours. `bash` (POSIX) buys you arrays, `[[ ]]`, and `pipefail`, which `/bin/sh` on Debian-family Linux does not have. Cannot be combined with `elevated: true` — that combination is rejected, not ignored. Machines running an AI Commander too old to understand this argument REFUSE the call outright rather than running the default shell behind your back; update the agent there, or drop the argument."),
213
216
  elevated: z
214
217
  .boolean()
215
218
  .optional()
@@ -697,5 +700,55 @@ server.registerTool("remote_job_cancel", {
697
700
  job_id: z.string().describe(JOB_ID_DESCRIPTION),
698
701
  },
699
702
  }, async ({ code, job_id }) => callTool("remote_job_cancel", { code, job_id }));
703
+ server.registerTool("remote_pull", {
704
+ description: "Copy a FILE off a remote machine so you (or the user) can actually open it — a training checkpoint, a " +
705
+ "rendered image, a CSV a job produced, a log too big to print. USE THIS instead of `cat`-ing a file through " +
706
+ "remote_exec: exec output is capped at 1 MiB and mangles binary, whereas this moves the real bytes. The " +
707
+ "machine reads the file and hands it to the relay, which stores it TEMPORARILY and returns a `blobId` plus a " +
708
+ "download link. TEMPORARY MEANS TEMPORARY: the stored copy is deleted 24 hours after it is created, whether " +
709
+ "or not anybody fetched it, and the download link stops working after ONE hour — the relay is a courier, not " +
710
+ "a file host, and nothing here is a backup, so pass the link on promptly and say that it expires. LIMIT: " +
711
+ "100 MiB per file; for a genuinely large artifact (a multi-GB checkpoint) have the JOB copy it to the user's " +
712
+ "own storage as its last step (`aws s3 cp`, `rclone`, `scp`) rather than splitting it into chunks. `path` " +
713
+ "must be ABSOLUTE and must name a regular file — a directory is refused, so `tar -czf` it first with " +
714
+ "remote_exec and pull the archive. Name the machine with `code` exactly as the user said it — an AIC- " +
715
+ "session code (e.g. AIC-XYZ-1234) or, when authenticated with an API key, a saved alias or hostname such as " +
716
+ "'wearfits-m3'; if the user's text contains 'aic-'/'AIC-' in any case, that is one of their machines.",
717
+ inputSchema: {
718
+ code: z.string().describe(MACHINE_CODE_DESCRIPTION),
719
+ path: z
720
+ .string()
721
+ .describe("ABSOLUTE path of the file on the remote machine, e.g. `/home/u/aic-jobs/train/out.ckpt` or " +
722
+ "`C:\\Users\\u\\out.png`. A relative path is refused rather than resolved against some working " +
723
+ "directory. Must be a regular file: archive a directory first and pull the archive."),
724
+ },
725
+ }, async ({ code, path }) => callTool("remote_pull", { code, path }));
726
+ server.registerTool("remote_push", {
727
+ description: "Write a stored file ONTO a remote machine — a dataset, a config, a model the user wants the box to work " +
728
+ "with. It takes a `blob_id`, NOT a local path: this tool moves bytes the relay is already holding, and it " +
729
+ "has no access to your own filesystem. A blob_id comes either from a previous remote_pull (so you can move a " +
730
+ "file between two of the user's machines) or from an upload the user makes themselves: " +
731
+ "`curl -X POST https://aicommander.dev/api/v1/files -H 'Authorization: Bearer <API key>' --data-binary " +
732
+ "@localfile`, which answers with the blobId. Uploading requires a signed-in account; an anonymous " +
733
+ "session-code caller can pull files off a machine but cannot push files to one. For small text files you do " +
734
+ "not need this at all — a remote_exec heredoc is simpler; use this for binary or anything over a few KB. " +
735
+ "`dest_path` must be ABSOLUTE, and the file is written ATOMICALLY (temp file, then rename), so a reader " +
736
+ "never sees a half-written file — but an EXISTING file at that path IS REPLACED, so confirm before " +
737
+ "overwriting. A blob expires 24 hours after it was created. Name the machine with `code` exactly as the user " +
738
+ "said it — an AIC- session code (e.g. AIC-XYZ-1234) or, when authenticated with an API key, a saved alias or " +
739
+ "hostname such as 'wearfits-m3'; if the user's text contains 'aic-'/'AIC-' in any case, that is one of their " +
740
+ "machines.",
741
+ inputSchema: {
742
+ code: z.string().describe(MACHINE_CODE_DESCRIPTION),
743
+ blob_id: z
744
+ .string()
745
+ .describe("The blobId from a previous remote_pull, or from POST /api/v1/files (32 hex characters). Blobs belong to " +
746
+ "the account that created them and expire after 24 hours."),
747
+ dest_path: z
748
+ .string()
749
+ .describe("ABSOLUTE destination path on the remote machine, e.g. `/home/u/data/train.csv`. The parent directory " +
750
+ "must already exist. An existing file at this path is REPLACED."),
751
+ },
752
+ }, async ({ code, blob_id, dest_path }) => callTool("remote_push", { code, blob_id, dest_path }));
700
753
  const transport = new StdioServerTransport();
701
754
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicommander/mcp",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "mcpName": "dev.aicommander/mcp",
5
5
  "description": "Remote shell and long-running background jobs for AI agents. Let Claude, Codex, ChatGPT or any MCP client run commands, builds, batch work and GPU/ML training on your own machines without exposed SSH, open ports or VPN.",
6
6
  "keywords": [
@@ -56,7 +56,7 @@
56
56
  "README.md"
57
57
  ],
58
58
  "engines": {
59
- "node": ">=18.0.0"
59
+ "node": ">=20.0.0"
60
60
  },
61
61
  "publishConfig": {
62
62
  "access": "public"