parallelsandbox-mcp 0.1.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 +43 -0
- package/index.mjs +91 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# parallelsandbox-mcp
|
|
2
|
+
|
|
3
|
+
stdio adapter for the ParallelSandbox MCP server. It exposes the same tools as `https://mcp.parallelsandbox.com/mcp`
|
|
4
|
+
(`sandbox_*`, `logs_*`), forwarding every call with your API key, and implements `sandbox_sync` locally so an agent
|
|
5
|
+
can upload its working directory into a box without committing.
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
PARALLELSANDBOX_API_KEY=psk_... npx parallelsandbox-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Claude Code:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
claude mcp add parallelsandbox -e PARALLELSANDBOX_API_KEY=psk_... -- npx -y parallelsandbox-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Any other MCP client: run the command above as a stdio server.
|
|
20
|
+
|
|
21
|
+
If you never need `sandbox_sync`, you can skip this package and connect directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
claude mcp add --transport http parallelsandbox https://mcp.parallelsandbox.com/mcp --header "Authorization: Bearer psk_..."
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Environment
|
|
28
|
+
|
|
29
|
+
| Variable | Default | Purpose |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `PARALLELSANDBOX_API_KEY` | required | API key from the app |
|
|
32
|
+
| `PARALLELSANDBOX_MCP_URL` | `https://mcp.parallelsandbox.com/mcp` | MCP endpoint (dev: `https://mcp.dev.parallelsandbox.com/mcp`) |
|
|
33
|
+
| `PARALLELSANDBOX_API_URL` | `https://api.parallelsandbox.com` | REST base used by `sandbox_sync` |
|
|
34
|
+
|
|
35
|
+
## sandbox_sync
|
|
36
|
+
|
|
37
|
+
`{"id": "<box id>", "localPath": ".", "dest": "repo"}` tars `localPath` (relative to the agent's working directory),
|
|
38
|
+
excluding `node_modules`, `.git`, `dist`, `build`, `coverage`, `.venv` and similar, and uploads it to `/work/<dest>` on the box.
|
|
39
|
+
|
|
40
|
+
## Timeouts
|
|
41
|
+
|
|
42
|
+
`sandbox_takeover` blocks until a person hands the box back, up to 30 minutes; the adapter waits 31 minutes for it and
|
|
43
|
+
65 minutes for other calls (a foreground `sandbox_exec` runs at most 60 minutes).
|
package/index.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// parallelsandbox-mcp: stdio in, ParallelSandbox Streamable HTTP out.
|
|
3
|
+
// Every tool call is forwarded to https://mcp.parallelsandbox.com/mcp with the API key, except sandbox_sync,
|
|
4
|
+
// which tars the local directory here and uploads it through POST /v1/boxes/{id}/sync.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, statSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
12
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
13
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
|
|
15
|
+
const API_KEY = process.env.PARALLELSANDBOX_API_KEY || "";
|
|
16
|
+
const MCP_URL = process.env.PARALLELSANDBOX_MCP_URL || "https://mcp.parallelsandbox.com/mcp";
|
|
17
|
+
const API_URL = (process.env.PARALLELSANDBOX_API_URL || "https://api.parallelsandbox.com").replace(/\/+$/, "");
|
|
18
|
+
const TAKEOVER_TIMEOUT_MS = 31 * 60 * 1000;
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 65 * 60 * 1000;
|
|
20
|
+
const SYNC_EXCLUDES = ["node_modules", ".git", "dist", "dist-web", "build", ".cache", "coverage", ".venv", "venv", "__pycache__", "target", ".next", ".turbo"];
|
|
21
|
+
|
|
22
|
+
const log = (...args) => console.error("[parallelsandbox-mcp]", ...args);
|
|
23
|
+
|
|
24
|
+
if (!API_KEY) {
|
|
25
|
+
log("PARALLELSANDBOX_API_KEY is required (create a key in the app at https://app.parallelsandbox.com)");
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const remote = new Client({ name: "parallelsandbox-mcp", version: "0.1.0" });
|
|
30
|
+
const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), {
|
|
31
|
+
requestInit: { headers: { Authorization: `Bearer ${API_KEY}` } },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
async function connectRemote() {
|
|
35
|
+
await remote.connect(transport);
|
|
36
|
+
log("connected to", MCP_URL);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function textResult(text, isError = false) {
|
|
40
|
+
return { content: [{ type: "text", text }], isError };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// sandbox_sync runs here: tar the local directory and upload it to the box through control.
|
|
44
|
+
async function sync(args) {
|
|
45
|
+
const { id, localPath, dest } = args || {};
|
|
46
|
+
if (!id || !localPath || !dest) {
|
|
47
|
+
return textResult("sandbox_sync needs id, localPath and dest", true);
|
|
48
|
+
}
|
|
49
|
+
const src = resolve(process.cwd(), localPath);
|
|
50
|
+
if (!existsSync(src) || !statSync(src).isDirectory()) {
|
|
51
|
+
return textResult(`local directory not found: ${src}`, true);
|
|
52
|
+
}
|
|
53
|
+
const tar = spawnSync("tar", ["-czf", "-", "-C", src, ...SYNC_EXCLUDES.map((e) => `--exclude=${e}`), "."], { maxBuffer: 2 * 1024 * 1024 * 1024 });
|
|
54
|
+
if (tar.status !== 0) {
|
|
55
|
+
return textResult(`tar failed: ${tar.stderr?.toString() || tar.status}`, true);
|
|
56
|
+
}
|
|
57
|
+
const res = await fetch(`${API_URL}/v1/boxes/${encodeURIComponent(id)}/sync?dest=${encodeURIComponent(dest)}`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/gzip" },
|
|
60
|
+
body: tar.stdout,
|
|
61
|
+
});
|
|
62
|
+
const body = await res.text();
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
let message = body;
|
|
65
|
+
try {
|
|
66
|
+
message = JSON.parse(body).error || body;
|
|
67
|
+
} catch {}
|
|
68
|
+
return textResult(`sync failed (HTTP ${res.status}): ${message}`, true);
|
|
69
|
+
}
|
|
70
|
+
const mb = (tar.stdout.length / 1024 / 1024).toFixed(1);
|
|
71
|
+
return textResult(JSON.stringify({ ok: true, dest, uploadedMB: Number(mb), excluded: SYNC_EXCLUDES }, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const server = new Server({ name: "parallelsandbox", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
75
|
+
|
|
76
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
77
|
+
const { tools } = await remote.listTools();
|
|
78
|
+
return { tools };
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
82
|
+
const { name, arguments: args } = req.params;
|
|
83
|
+
if (name === "sandbox_sync") {
|
|
84
|
+
return sync(args);
|
|
85
|
+
}
|
|
86
|
+
const timeout = name === "sandbox_takeover" ? TAKEOVER_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
|
|
87
|
+
return remote.callTool({ name, arguments: args || {} }, undefined, { timeout, resetTimeoutOnProgress: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await connectRemote();
|
|
91
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "parallelsandbox-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "stdio adapter for the ParallelSandbox MCP server: runs locally so sandbox_sync can upload your working directory",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"parallelsandbox-mcp": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/parallel-sandbox/parallelsandbox_control_server",
|
|
20
|
+
"directory": "packages/parallelsandbox-mcp"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.19.1"
|
|
24
|
+
}
|
|
25
|
+
}
|