@aicommander/mcp 1.0.52 → 1.0.54
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 +8 -2
- package/dist/bin/mcp.js +77 -111
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ 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 `
|
|
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
|
|
|
@@ -19,16 +19,20 @@ Use this package to connect any MCP client that speaks **stdio** (Codex CLI, Cla
|
|
|
19
19
|
|---|---|
|
|
20
20
|
| `remote_exec` | Run a shell command on one of your machines — the tool for any "connect to / remote shell / run X on" request. Name the machine by its `AIC-…` session code or (with an API key) a saved alias/hostname like `wearfits-m3`. Output is **buffered**: stdout and stderr come back in one reply once the command finishes, and if the call is cut short (timeout, agent error, agent disconnect) you get whatever had been buffered, marked PARTIAL — an unknown outcome, not an empty failure. Optional `env` (string→string) sets variables for that one command; it is refused together with `elevated: true`. |
|
|
21
21
|
| `session_status` | Check whether a machine is online/active/reachable (e.g. "is wearfits-m3 up?"). Same machine naming as above. Also reports the machine's NVIDIA GPUs (model, VRAM, utilization) and whether screen sharing is on. |
|
|
22
|
-
| `list_machines` | List
|
|
22
|
+
| `list_machines` | List every machine saved to your account, including independent `blocked` and `planRestricted` states. Free keeps all records but only the 10 oldest by creation time, then id, are usable; deleting one promotes the next, while Pro enables all saved records up to the technical ceiling of 100. Restricted records are not probed and deliberately carry no live telemetry (`online:false`, `lastSeenAt:null`, platform/agent/GPU omitted), so do not interpret them as offline or try another tool against them: it fails with `reason:"plan_device_limit"`. Unrestricted entries include live/last-known platform and GPU details. Requires an API key (`AICOMMANDER_TOKEN`); takes no arguments. |
|
|
23
23
|
| `remote_job_start` | Start a **long-running** command as a detached background job — training, fine-tuning, dataset processing, long builds. Returns a `jobId` immediately and keeps running after the call and the conversation. A job outlives the agent process everywhere; what differs is what can still take it down: macOS reparents to PID 1 and is out of reach of anything aimed at the app (even a tree kill), Windows survives the agent process dying by itself (a crash, or a `taskkill /F /IM` without `/T`) but **loses the job to a tree kill** ("End task", `taskkill /T`, an installer that stops the app and everything it started — assume an auto-update is one unless that machine's installer is known to do otherwise), Linux stops with the systemd service. `gpu_index` reserves one NVIDIA card exclusively (sets `CUDA_VISIBLE_DEVICES`; a second job wanting that card is refused `gpu_busy`, and an index that is not on the machine's known GPU list is rejected instead of starting a phantom job). |
|
|
24
24
|
| `remote_job_list` | The machine's jobs: running now, plus finished ones still retained (7 days). Answers "what is running on the GPU box?" and recovers a `jobId` from an earlier conversation. `limit` returns only the newest N (default 20) so a busy machine doesn't dump dozens of entries into your context; the reply says how many were omitted. |
|
|
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` | **Pro only.** Free and anonymous callers cannot start a transfer. Copy a **file** off one of your machines — a checkpoint, render, CSV or large log. Use it instead of `cat` through `remote_exec`, whose reply is capped at 1 MiB and mangles binary. The relay returns a `blobId` and download link: the link stops working after **1 hour** and the stored copy after **24 hours**, fetched or not. A blob/link created before downgrade remains usable only until that original TTL; neither can be renewed. Limit **100 MiB**; use storage you control for larger artifacts. `path` must be absolute and name a regular file. |
|
|
29
|
+
| `remote_push` | **Pro only.** Free and anonymous callers cannot start a transfer, and `POST /api/v1/files` upload is Pro-only too. Write a stored blob **onto** a machine. Blob ids come from a previous `remote_pull` or a Pro upload with `curl -X POST https://aicommander.dev/api/v1/files -H 'Authorization: Bearer <API key>' --data-binary @localfile`. `dest_path` must be absolute; an existing file is replaced atomically with the documented metadata safety rules. Blobs stop being readable 24 hours after creation; a blob created before downgrade keeps only that original TTL and cannot be renewed. |
|
|
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
|
|
|
34
|
+
> **File transfers require Pro.** Free and anonymous callers cannot upload, pull, or push. Allowed calls answer within about **55 seconds**; slower work fails instead of outliving the MCP client's usual 60-second request. Pro transfers are quota'd, and over a limit the answer is `429` with `reason: "rate_limited"`: **60 transfers an hour per account** (pulls and pushes together), 60 uploads an hour, and **5 GiB of relay storage per account per rolling day** charged by uploads and pulls, not pushes. Pull reserves the full **100 MiB** maximum until settlement, so low headroom or concurrent pulls can refuse a smaller file. Existing blobs retain only their original 24-hour TTL after downgrade; existing download links retain only their original one-hour TTL.
|
|
35
|
+
|
|
32
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.
|
|
33
37
|
>
|
|
34
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.
|
|
@@ -41,6 +45,8 @@ These are the canonical way to reach your machines — your AI client should use
|
|
|
41
45
|
|
|
42
46
|
## Configuration
|
|
43
47
|
|
|
48
|
+
Requires **Node ≥20** wherever the bridge is launched.
|
|
49
|
+
|
|
44
50
|
Two environment variables:
|
|
45
51
|
|
|
46
52
|
| Variable | Required | Default | Description |
|
package/dist/bin/mcp.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolResultSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { readFileSync } from "node:fs";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -243,21 +244,23 @@ server.registerTool("remote_exec", {
|
|
|
243
244
|
isError: true,
|
|
244
245
|
};
|
|
245
246
|
}
|
|
246
|
-
// Parse SSE stream
|
|
247
|
+
// Parse the one terminal tools/call result from the SSE stream. Heartbeats
|
|
248
|
+
// are comments and therefore absent here; keeping the hosted result intact is
|
|
249
|
+
// important because plan/identity refusals carry both isError and a stable
|
|
250
|
+
// reason in structuredContent.
|
|
247
251
|
const text = await res.text();
|
|
248
252
|
const lines = text.split("\n");
|
|
249
|
-
|
|
253
|
+
let result;
|
|
250
254
|
for (const line of lines) {
|
|
251
255
|
if (!line.startsWith("data: "))
|
|
252
256
|
continue;
|
|
253
257
|
try {
|
|
254
258
|
const data = JSON.parse(line.slice(6));
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
259
|
+
if (data?.result !== undefined) {
|
|
260
|
+
const parsed = CallToolResultSchema.safeParse(data.result);
|
|
261
|
+
if (!parsed.success)
|
|
262
|
+
return unusableHostedResult();
|
|
263
|
+
result = parsed.data;
|
|
261
264
|
}
|
|
262
265
|
// Error
|
|
263
266
|
if (data?.error) {
|
|
@@ -273,9 +276,7 @@ server.registerTool("remote_exec", {
|
|
|
273
276
|
// ignore non-JSON lines
|
|
274
277
|
}
|
|
275
278
|
}
|
|
276
|
-
return
|
|
277
|
-
content: [{ type: "text", text: output.join("") || "(no output)" }],
|
|
278
|
-
};
|
|
279
|
+
return result ?? unusableHostedResult();
|
|
279
280
|
});
|
|
280
281
|
// Tool: session_status
|
|
281
282
|
server.registerTool("session_status", {
|
|
@@ -304,68 +305,12 @@ server.registerTool("session_status", {
|
|
|
304
305
|
.string()
|
|
305
306
|
.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'."),
|
|
306
307
|
},
|
|
307
|
-
}, async ({ code }) => {
|
|
308
|
-
const res = await fetch(MCP_URL, {
|
|
309
|
-
method: "POST",
|
|
310
|
-
headers: {
|
|
311
|
-
"Content-Type": "application/json",
|
|
312
|
-
...(ACCOUNT_TOKEN ? { Authorization: `Bearer ${ACCOUNT_TOKEN}` } : {}),
|
|
313
|
-
},
|
|
314
|
-
body: JSON.stringify({
|
|
315
|
-
jsonrpc: "2.0",
|
|
316
|
-
id: 1,
|
|
317
|
-
method: "tools/call",
|
|
318
|
-
params: { name: "session_status", arguments: { code } },
|
|
319
|
-
}),
|
|
320
|
-
});
|
|
321
|
-
if (!res.ok) {
|
|
322
|
-
const text = await res.text();
|
|
323
|
-
return {
|
|
324
|
-
content: [{ type: "text", text: `Error ${res.status}: ${text}` }],
|
|
325
|
-
isError: true,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
const data = (await res.json());
|
|
329
|
-
const content = data?.result?.content ?? [
|
|
330
|
-
{ type: "text", text: JSON.stringify(data) },
|
|
331
|
-
];
|
|
332
|
-
return {
|
|
333
|
-
content: content,
|
|
334
|
-
};
|
|
335
|
-
});
|
|
308
|
+
}, async ({ code }) => callTool("session_status", { code }));
|
|
336
309
|
// Tool: list_machines
|
|
337
310
|
server.registerTool("list_machines", {
|
|
338
|
-
description: "List ALL of the user's machines
|
|
311
|
+
description: "List ALL of the user's saved machines with each one's access state and, when available, live status. USE THIS for fleet-wide questions or when the user has not named a machine yet. Requires an API key (set AICOMMANDER_TOKEN); without an account there is no list, so fall back to session_status with a specific AIC- code. Each entry includes `blocked` (operator approval/block state) and `planRestricted` (account-plan state); these are independent and can both be true. Free keeps every saved record but only the 10 oldest by creation time, then id, are usable. Newer records have `planRestricted:true`; deleting an older record promotes the next oldest, or upgrading to Pro restores all saved records up to the technical 100-record ceiling. NEVER attempt another tool against a plan-restricted entry: it fails with `reason:\"plan_device_limit\"`. Restricted entries intentionally expose no liveness or telemetry: `online:false`, `lastSeenAt:null`, with platform, agent version and GPU details omitted. This is not evidence that the machine itself is offline. For an unrestricted entry, platform selects the shell dialect ('win32' means cmd.exe), offline platform/GPU data is last-known, and GPU details help pick a compute box. Takes no arguments.",
|
|
339
312
|
inputSchema: {},
|
|
340
|
-
}, async () => {
|
|
341
|
-
const res = await fetch(MCP_URL, {
|
|
342
|
-
method: "POST",
|
|
343
|
-
headers: {
|
|
344
|
-
"Content-Type": "application/json",
|
|
345
|
-
...(ACCOUNT_TOKEN ? { Authorization: `Bearer ${ACCOUNT_TOKEN}` } : {}),
|
|
346
|
-
},
|
|
347
|
-
body: JSON.stringify({
|
|
348
|
-
jsonrpc: "2.0",
|
|
349
|
-
id: 1,
|
|
350
|
-
method: "tools/call",
|
|
351
|
-
params: { name: "list_machines", arguments: {} },
|
|
352
|
-
}),
|
|
353
|
-
});
|
|
354
|
-
if (!res.ok) {
|
|
355
|
-
const text = await res.text();
|
|
356
|
-
return {
|
|
357
|
-
content: [{ type: "text", text: `Error ${res.status}: ${text}` }],
|
|
358
|
-
isError: true,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
const data = (await res.json());
|
|
362
|
-
const content = data?.result?.content ?? [
|
|
363
|
-
{ type: "text", text: JSON.stringify(data) },
|
|
364
|
-
];
|
|
365
|
-
return {
|
|
366
|
-
content: content,
|
|
367
|
-
};
|
|
368
|
-
});
|
|
313
|
+
}, async () => callTool("list_machines", {}));
|
|
369
314
|
// --- Shared forwarding helper ----------------------------------------------
|
|
370
315
|
//
|
|
371
316
|
// Every tool except remote_exec is plain request/response JSON-RPC on the Worker
|
|
@@ -374,38 +319,18 @@ server.registerTool("list_machines", {
|
|
|
374
319
|
/** How the machine is named — identical wording across the job tools. */
|
|
375
320
|
const MACHINE_CODE_DESCRIPTION = "How the user named the machine — pass it exactly as given (AIC- session code, or a saved alias/hostname when authenticated with an API key).";
|
|
376
321
|
const JOB_ID_DESCRIPTION = "The jobId returned by remote_job_start (16 hex characters).";
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
* required fields would be rejected by the client's own schema validation, losing
|
|
383
|
-
* the whole reply rather than one block. Fail closed, per block.
|
|
384
|
-
*/
|
|
385
|
-
function normalizeContent(blocks) {
|
|
386
|
-
if (!Array.isArray(blocks))
|
|
387
|
-
return [];
|
|
388
|
-
const out = [];
|
|
389
|
-
for (const raw of blocks) {
|
|
390
|
-
if (typeof raw !== "object" || raw === null)
|
|
391
|
-
continue;
|
|
392
|
-
const b = raw;
|
|
393
|
-
if (b["type"] === "text" && typeof b["text"] === "string") {
|
|
394
|
-
out.push({ type: "text", text: b["text"] });
|
|
395
|
-
}
|
|
396
|
-
else if (b["type"] === "image" &&
|
|
397
|
-
typeof b["data"] === "string" &&
|
|
398
|
-
typeof b["mimeType"] === "string") {
|
|
399
|
-
out.push({ type: "image", data: b["data"], mimeType: b["mimeType"] });
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return out;
|
|
322
|
+
function unusableHostedResult() {
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: "text", text: "The server returned no usable tool result." }],
|
|
325
|
+
isError: true,
|
|
326
|
+
};
|
|
403
327
|
}
|
|
404
328
|
/**
|
|
405
|
-
* Forward one tool call to the Worker `/mcp` endpoint and hand back its
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
329
|
+
* Forward one tool call to the Worker `/mcp` endpoint and hand back its complete
|
|
330
|
+
* MCP result unchanged after schema validation. That includes content blocks,
|
|
331
|
+
* isError, and structuredContent: callers rely on stable refusal reasons there
|
|
332
|
+
* (including plan_device_limit), so dropping either flag makes a denial resemble a
|
|
333
|
+
* successful no-op.
|
|
409
334
|
*/
|
|
410
335
|
async function callTool(name, args) {
|
|
411
336
|
const res = await fetch(MCP_URL, {
|
|
@@ -435,17 +360,8 @@ async function callTool(name, args) {
|
|
|
435
360
|
isError: true,
|
|
436
361
|
};
|
|
437
362
|
}
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
// all; say so instead, and mark it an error so the reply is not mistaken for a
|
|
441
|
-
// successful no-op.
|
|
442
|
-
if (content.length === 0) {
|
|
443
|
-
return {
|
|
444
|
-
content: [{ type: "text", text: "The server returned no usable content." }],
|
|
445
|
-
isError: true,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
return data?.result?.isError ? { content, isError: true } : { content };
|
|
363
|
+
const parsed = CallToolResultSchema.safeParse(data?.result);
|
|
364
|
+
return parsed.success ? parsed.data : unusableHostedResult();
|
|
449
365
|
}
|
|
450
366
|
// Tool: remote_screenshot
|
|
451
367
|
server.registerTool("remote_screenshot", {
|
|
@@ -700,5 +616,55 @@ server.registerTool("remote_job_cancel", {
|
|
|
700
616
|
job_id: z.string().describe(JOB_ID_DESCRIPTION),
|
|
701
617
|
},
|
|
702
618
|
}, async ({ code, job_id }) => callTool("remote_job_cancel", { code, job_id }));
|
|
619
|
+
server.registerTool("remote_pull", {
|
|
620
|
+
description: "PRO PLAN ONLY. Free and anonymous callers cannot start file transfers; tell a Free user to upgrade and an anonymous user to sign in with Pro. A plan-restricted machine fails with `reason:\"plan_device_limit\"`. Copy a FILE off a remote machine so you (or the user) can actually open it — a training checkpoint, a " +
|
|
621
|
+
"rendered image, a CSV a job produced, a log too big to print. USE THIS instead of `cat`-ing a file through " +
|
|
622
|
+
"remote_exec: exec output is capped at 1 MiB and mangles binary, whereas this moves the real bytes. The " +
|
|
623
|
+
"machine reads the file and hands it to the relay, which stores it TEMPORARILY and returns a `blobId` plus a " +
|
|
624
|
+
"download link. A blob/link created while transfer was allowed remains usable only until its existing TTL after downgrade; it cannot be renewed. TEMPORARY MEANS TEMPORARY: the stored copy stops being readable 24 hours after it is created, whether " +
|
|
625
|
+
"or not anybody fetched it, and the download link stops working after ONE hour. An hourly, retrying sweep " +
|
|
626
|
+
"removes inaccessible expired bytes afterward — the relay is a courier, not " +
|
|
627
|
+
"a file host, and nothing here is a backup, so pass the link on promptly and say that it expires. LIMIT: " +
|
|
628
|
+
"100 MiB per file; for a genuinely large artifact (a multi-GB checkpoint) have the JOB copy it to the user's " +
|
|
629
|
+
"own storage as its last step (`aws s3 cp`, `rclone`, `scp`) rather than splitting it into chunks. `path` " +
|
|
630
|
+
"must be ABSOLUTE and must name a regular file — a directory is refused, so `tar -czf` it first with " +
|
|
631
|
+
"remote_exec and pull the archive. Name the machine with `code` exactly as the user said it — an AIC- " +
|
|
632
|
+
"session code (e.g. AIC-XYZ-1234) or, when authenticated with an API key, a saved alias or hostname such as " +
|
|
633
|
+
"'wearfits-m3'; if the user's text contains 'aic-'/'AIC-' in any case, that is one of their machines.",
|
|
634
|
+
inputSchema: {
|
|
635
|
+
code: z.string().describe(MACHINE_CODE_DESCRIPTION),
|
|
636
|
+
path: z
|
|
637
|
+
.string()
|
|
638
|
+
.describe("ABSOLUTE path of the file on the remote machine, e.g. `/home/u/aic-jobs/train/out.ckpt` or " +
|
|
639
|
+
"`C:\\Users\\u\\out.png`. A relative path is refused rather than resolved against some working " +
|
|
640
|
+
"directory. Must be a regular file: archive a directory first and pull the archive."),
|
|
641
|
+
},
|
|
642
|
+
}, async ({ code, path }) => callTool("remote_pull", { code, path }));
|
|
643
|
+
server.registerTool("remote_push", {
|
|
644
|
+
description: "PRO PLAN ONLY. Free and anonymous callers cannot start file transfers; tell a Free user to upgrade and an anonymous user to sign in with Pro. A plan-restricted machine fails with `reason:\"plan_device_limit\"`. Write a stored file ONTO a remote machine — a dataset, a config, a model the user wants the box to work " +
|
|
645
|
+
"with. It takes a `blob_id`, NOT a local path: this tool moves bytes the relay is already holding, and it " +
|
|
646
|
+
"has no access to your own filesystem. A blob_id comes either from a previous remote_pull (so you can move a " +
|
|
647
|
+
"file between two of the user's machines) or from an upload the user makes themselves: " +
|
|
648
|
+
"`curl -X POST https://aicommander.dev/api/v1/files -H 'Authorization: Bearer <API key>' --data-binary " +
|
|
649
|
+
"@localfile`, which answers with the blobId. That upload is also Pro-only; Free and anonymous callers are denied. For small text files you do " +
|
|
650
|
+
"not need this at all — a remote_exec heredoc is simpler; use this for binary or anything over a few KB. " +
|
|
651
|
+
"`dest_path` must be ABSOLUTE, and the file is written ATOMICALLY (temp file, then rename), so a reader " +
|
|
652
|
+
"never sees a half-written file — but an EXISTING file at that path IS REPLACED, so confirm before " +
|
|
653
|
+
"overwriting. A blob expires 24 hours after it was created; a blob created before downgrade remains usable only until that original TTL and cannot be renewed. Name the machine with `code` exactly as the user " +
|
|
654
|
+
"said it — an AIC- session code (e.g. AIC-XYZ-1234) or, when authenticated with an API key, a saved alias or " +
|
|
655
|
+
"hostname such as 'wearfits-m3'; if the user's text contains 'aic-'/'AIC-' in any case, that is one of their " +
|
|
656
|
+
"machines.",
|
|
657
|
+
inputSchema: {
|
|
658
|
+
code: z.string().describe(MACHINE_CODE_DESCRIPTION),
|
|
659
|
+
blob_id: z
|
|
660
|
+
.string()
|
|
661
|
+
.describe("The blobId from a previous remote_pull, or from POST /api/v1/files (32 hex characters). Blobs belong to " +
|
|
662
|
+
"the account that created them and expire after 24 hours."),
|
|
663
|
+
dest_path: z
|
|
664
|
+
.string()
|
|
665
|
+
.describe("ABSOLUTE destination path on the remote machine, e.g. `/home/u/data/train.csv`. The parent directory " +
|
|
666
|
+
"must already exist. An existing file at this path is REPLACED."),
|
|
667
|
+
},
|
|
668
|
+
}, async ({ code, blob_id, dest_path }) => callTool("remote_push", { code, blob_id, dest_path }));
|
|
703
669
|
const transport = new StdioServerTransport();
|
|
704
670
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aicommander/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.54",
|
|
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": ">=
|
|
59
|
+
"node": ">=20.0.0"
|
|
60
60
|
},
|
|
61
61
|
"publishConfig": {
|
|
62
62
|
"access": "public"
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "tsc",
|
|
66
66
|
"prepublishOnly": "tsc",
|
|
67
|
+
"test": "pnpm run build && node --test test/*.test.mjs",
|
|
67
68
|
"typecheck": "tsc --noEmit"
|
|
68
69
|
},
|
|
69
70
|
"dependencies": {
|