@tomasko/mcpbox 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +96 -2
  2. package/bin/mcpbox.js +58 -0
  3. package/lib.js +270 -0
  4. package/package.json +43 -4
package/README.md CHANGED
@@ -1,3 +1,97 @@
1
- # Temporary Holding Version
1
+ # `@tomasko/mcpbox`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The npm launcher for [`mcpbox`](https://github.com/tomasky/mcpbox) — a
4
+ self-contained MCP (Model Context Protocol) client CLI in the spirit of
5
+ `mcporter`, written in MoonBit.
6
+
7
+ ```sh
8
+ npm install -g @tomasko/mcpbox
9
+ mcpbox list
10
+ mcpbox call playwright.browser_close
11
+ mcpbox call adhoc.echo --stdio "python3 echo_server.py" name=hi count=5
12
+ ```
13
+
14
+ `mcpbox` discovers the MCP servers you already have configured — in `mcpbox`
15
+ files, Cursor, Claude Code/Desktop, Windsurf or VS Code — connects to them over
16
+ stdio or Streamable HTTP, and lists or calls their tools from a shell, a script
17
+ or an agent. One process, no daemon, no background state.
18
+
19
+ ## What this package contains
20
+
21
+ A small launcher, not the binary itself. On the first `mcpbox` run it downloads
22
+ the prebuilt native binary for your platform from the GitHub release and caches
23
+ it. Nothing is downloaded at install time.
24
+
25
+ | Platform | Target |
26
+ | --- | --- |
27
+ | Linux x86-64 | `mcpbox-linux-x86_64` (static, musl) |
28
+ | Linux arm64 | `mcpbox-linux-aarch64` (static, musl) |
29
+ | macOS Apple silicon | `mcpbox-darwin-aarch64` |
30
+
31
+ The Linux builds are linked statically against musl, so they run on any
32
+ distribution regardless of its glibc version.
33
+
34
+ ## Why there is no `postinstall`
35
+
36
+ This package deliberately declares **no install script**. Since npm 12,
37
+ dependency lifecycle scripts are blocked unless the user opts in; and pnpm 12
38
+ fails a *project* install outright with `ERR_PNPM_IGNORED_BUILDS` when a
39
+ dependency declares one:
40
+
41
+ ```console
42
+ $ pnpm add @tomasko/mcpbox
43
+ Error: ERR_PNPM_IGNORED_BUILDS
44
+ × adding a new package
45
+ ╰─▶ Ignored build scripts: @tomasko/mcpbox@0.1.1
46
+ ```
47
+
48
+ Downloading lazily from `bin/mcpbox.js` sidesteps all of it: it works under
49
+ npm, pnpm, yarn, bun, `npx`, and `--ignore-scripts`, because it is an ordinary
50
+ command rather than an install hook.
51
+
52
+ ## Cache location
53
+
54
+ The downloaded binary is kept outside the package, so reinstalling or clearing
55
+ `node_modules` never triggers a second download:
56
+
57
+ | Platform | Path |
58
+ | --- | --- |
59
+ | Linux | `$XDG_CACHE_HOME/mcpbox` or `~/.cache/mcpbox` |
60
+ | macOS | `~/Library/Caches/mcpbox` |
61
+
62
+ ## Environment variables
63
+
64
+ | Variable | Effect |
65
+ | --- | --- |
66
+ | `MCPBOX_BINARY` | Run this binary and never download anything |
67
+ | `MCPBOX_CACHE_DIR` | Where to keep the downloaded binary |
68
+ | `MCPBOX_VERSION` | Release version to fetch (default: this package's version) |
69
+ | `MCPBOX_REPO` | `owner/repo` to download from (default: `tomasky/mcpbox`) |
70
+ | `MCPBOX_DOWNLOAD_BASE` | Full base URL, overriding `MCPBOX_REPO`/`MCPBOX_VERSION` |
71
+ | `MCPBOX_FORCE_DOWNLOAD` | Re-download even if the cached binary exists |
72
+ | `MCPBOX_SKIP_DOWNLOAD` | Never download; fail if the binary is absent |
73
+ | `MCPBOX_NO_VERIFY` | Skip SHA-256 verification of the download |
74
+
75
+ `MCPBOX_DOWNLOAD_BASE` and `MCPBOX_REPO` are there for forks, private mirrors
76
+ and air-gapped installs:
77
+
78
+ ```sh
79
+ MCPBOX_DOWNLOAD_BASE=https://internal.example.com/mcpbox npm install -g @tomasko/mcpbox
80
+ ```
81
+
82
+ The CLI itself also reads `MCPBOX_CONFIG` to point at an explicit config file.
83
+
84
+ ## Other ways to install
85
+
86
+ ```sh
87
+ # Installer script (no Node.js required)
88
+ curl -LsSf https://raw.githubusercontent.com/tomasky/mcpbox/master/install.sh | bash
89
+
90
+ # Build from source
91
+ git clone https://github.com/tomasky/mcpbox && cd mcpbox
92
+ moon build --target native --release
93
+ ```
94
+
95
+ ## License
96
+
97
+ Apache-2.0
package/bin/mcpbox.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // Launcher for the native `mcpbox` binary.
3
+ //
4
+ // The package ships no install script, so this is where the binary is fetched:
5
+ // on the first run, straight into the user cache. Arguments, stdio, signals and
6
+ // the exit code are all forwarded to the real binary.
7
+ "use strict";
8
+
9
+ const { spawn } = require("child_process");
10
+ const { ensureBinary } = require("../lib.js");
11
+
12
+ const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
13
+
14
+ async function main() {
15
+ let binary;
16
+ try {
17
+ binary = await ensureBinary({
18
+ log: (line) => process.stderr.write(`${line}\n`),
19
+ });
20
+ } catch (err) {
21
+ process.stderr.write(`mcpbox: ${err.message}\n`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
26
+
27
+ // Forward signals so Ctrl-C reaches the CLI rather than killing only the
28
+ // wrapper and orphaning the child.
29
+ for (const signal of SIGNALS) {
30
+ process.on(signal, () => {
31
+ try {
32
+ child.kill(signal);
33
+ } catch {
34
+ /* already gone */
35
+ }
36
+ });
37
+ }
38
+
39
+ child.on("error", (err) => {
40
+ process.stderr.write(`mcpbox: failed to launch ${binary}: ${err.message}\n`);
41
+ process.exit(1);
42
+ });
43
+
44
+ child.on("exit", (code, signal) => {
45
+ if (signal) {
46
+ // Mirror the child's death by signal instead of inventing an exit code.
47
+ process.removeAllListeners(signal);
48
+ process.kill(process.pid, signal);
49
+ return;
50
+ }
51
+ process.exit(code === null ? 1 : code);
52
+ });
53
+ }
54
+
55
+ main().catch((err) => {
56
+ process.stderr.write(`mcpbox: ${err && err.stack ? err.stack : err}\n`);
57
+ process.exit(1);
58
+ });
package/lib.js ADDED
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+
3
+ // Shared logic for the npm launcher: work out which prebuilt `mcpbox` binary
4
+ // belongs on this machine, and fetch it from GitHub Releases on demand.
5
+ //
6
+ // mcpbox deliberately ships **no install script**. As of npm 12 dependency
7
+ // lifecycle scripts are blocked unless the user opts in, and pnpm 12 fails a
8
+ // *project* install outright with ERR_PNPM_IGNORED_BUILDS when a dependency
9
+ // declares one. Downloading on first run works under every package manager,
10
+ // under `--ignore-scripts`, and under `npx`, so that is what this does.
11
+ //
12
+ // Overrides (all optional):
13
+ // MCPBOX_REPO owner/repo (default: tomasky/mcpbox)
14
+ // MCPBOX_VERSION release version (default: this package's version)
15
+ // MCPBOX_DOWNLOAD_BASE full base URL (overrides MCPBOX_REPO/MCPBOX_VERSION)
16
+ // MCPBOX_CACHE_DIR where the binary is kept
17
+ // MCPBOX_BINARY run this binary and never download
18
+ // MCPBOX_FORCE_DOWNLOAD re-download even if the binary is already present
19
+ // MCPBOX_SKIP_DOWNLOAD never download; fail if the binary is absent
20
+ // MCPBOX_NO_VERIFY skip SHA-256 verification
21
+
22
+ const crypto = require("crypto");
23
+ const fs = require("fs");
24
+ const os = require("os");
25
+ const path = require("path");
26
+ const https = require("https");
27
+ const http = require("http");
28
+
29
+ const pkg = require("./package.json");
30
+
31
+ // Node's `platform-arch` -> the release target name. Intel macOS is absent on
32
+ // purpose: the MoonBit toolchain no longer ships a darwin-x86_64 target.
33
+ const TARGETS = {
34
+ "linux-x64": "linux-x86_64",
35
+ "linux-arm64": "linux-aarch64",
36
+ "darwin-arm64": "darwin-aarch64",
37
+ };
38
+
39
+ const SUPPORTED = "linux-x86_64, linux-aarch64 and darwin-aarch64";
40
+
41
+ function targetName() {
42
+ return TARGETS[`${process.platform}-${process.arch}`];
43
+ }
44
+
45
+ function repoSlug() {
46
+ if (process.env.MCPBOX_REPO) return process.env.MCPBOX_REPO;
47
+ const url =
48
+ typeof pkg.repository === "string"
49
+ ? pkg.repository
50
+ : (pkg.repository && pkg.repository.url) || "";
51
+ const m = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
52
+ return m ? `${m[1]}/${m[2]}` : "tomasky/mcpbox";
53
+ }
54
+
55
+ function version() {
56
+ return (process.env.MCPBOX_VERSION || pkg.version).replace(/^v/, "");
57
+ }
58
+
59
+ function assetName(target) {
60
+ return `mcpbox-${target}`;
61
+ }
62
+
63
+ // Both URLs are tried in order: the pinned version first, then whatever is
64
+ // latest. The fallback keeps the launcher useful when a release was re-cut
65
+ // under a different tag, or when package.json was bumped ahead of the tag.
66
+ function downloadUrls(target) {
67
+ const asset = assetName(target);
68
+ if (process.env.MCPBOX_DOWNLOAD_BASE) {
69
+ const base = process.env.MCPBOX_DOWNLOAD_BASE.replace(/\/+$/, "");
70
+ return [`${base}/${asset}`];
71
+ }
72
+ const repo = repoSlug();
73
+ return [
74
+ `https://github.com/${repo}/releases/download/v${version()}/${asset}`,
75
+ `https://github.com/${repo}/releases/latest/download/${asset}`,
76
+ ];
77
+ }
78
+
79
+ function cacheDir() {
80
+ if (process.env.MCPBOX_CACHE_DIR) return process.env.MCPBOX_CACHE_DIR;
81
+ const home = os.homedir();
82
+ if (process.platform === "darwin") {
83
+ return path.join(home, "Library", "Caches", "mcpbox");
84
+ }
85
+ const base = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
86
+ return path.join(base, "mcpbox");
87
+ }
88
+
89
+ function binaryPath(target) {
90
+ return path.join(cacheDir(), `mcpbox-${version()}-${target}`);
91
+ }
92
+
93
+ function isRunnable(file) {
94
+ try {
95
+ fs.accessSync(file, fs.constants.X_OK);
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ // --- HTTP ------------------------------------------------------------------
103
+
104
+ function fetch(url, redirects) {
105
+ return new Promise((resolve, reject) => {
106
+ if (redirects > 10) {
107
+ reject(new Error("too many redirects"));
108
+ return;
109
+ }
110
+ const client = url.startsWith("http:") ? http : https;
111
+ const req = client.get(
112
+ url,
113
+ { headers: { "user-agent": `mcpbox-npm/${pkg.version}` } },
114
+ (res) => {
115
+ const { statusCode, headers } = res;
116
+ if (statusCode >= 300 && statusCode < 400 && headers.location) {
117
+ res.resume();
118
+ resolve(fetch(new URL(headers.location, url).toString(), redirects + 1));
119
+ return;
120
+ }
121
+ if (statusCode !== 200) {
122
+ res.resume();
123
+ const err = new Error(`GET ${url} -> HTTP ${statusCode}`);
124
+ err.statusCode = statusCode;
125
+ reject(err);
126
+ return;
127
+ }
128
+ resolve(res);
129
+ }
130
+ );
131
+ req.on("error", reject);
132
+ req.setTimeout(120000, () => req.destroy(new Error(`timed out fetching ${url}`)));
133
+ });
134
+ }
135
+
136
+ async function fetchBuffer(url) {
137
+ const res = await fetch(url, 0);
138
+ const chunks = [];
139
+ for await (const chunk of res) chunks.push(chunk);
140
+ return Buffer.concat(chunks);
141
+ }
142
+
143
+ async function fetchText(url) {
144
+ return (await fetchBuffer(url)).toString("utf8");
145
+ }
146
+
147
+ // --- install ---------------------------------------------------------------
148
+
149
+ // Download to a sibling temp file and rename into place, so a crashed or
150
+ // concurrent run can never leave a half-written binary at `dest`.
151
+ async function downloadTo(url, dest) {
152
+ const res = await fetch(url, 0);
153
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
154
+ const tmp = `${dest}.${process.pid}.${Date.now()}.part`;
155
+ try {
156
+ await new Promise((resolve, reject) => {
157
+ const out = fs.createWriteStream(tmp, { mode: 0o755 });
158
+ res.pipe(out);
159
+ out.on("finish", () => out.close(resolve));
160
+ out.on("error", reject);
161
+ res.on("error", reject);
162
+ });
163
+ const size = fs.statSync(tmp).size;
164
+ if (size === 0) throw new Error(`${url} returned an empty file`);
165
+ fs.chmodSync(tmp, 0o755);
166
+ fs.renameSync(tmp, dest);
167
+ } catch (err) {
168
+ fs.rmSync(tmp, { force: true });
169
+ throw err;
170
+ }
171
+ }
172
+
173
+ // Verify against the `<asset>.sha256` published next to the binary. A missing
174
+ // checksum is a warning, not a failure, so older releases keep working.
175
+ async function verify(dest, url, log) {
176
+ if (process.env.MCPBOX_NO_VERIFY) return;
177
+ let published;
178
+ try {
179
+ published = (await fetchText(`${url}.sha256`)).trim();
180
+ } catch {
181
+ log(`mcpbox: warning: no checksum published for ${path.basename(url)}`);
182
+ return;
183
+ }
184
+ const expected = (published.split(/\s+/)[0] || "").toLowerCase();
185
+ if (!/^[0-9a-f]{64}$/.test(expected)) {
186
+ log("mcpbox: warning: published checksum is not a SHA-256 digest");
187
+ return;
188
+ }
189
+ const actual = crypto.createHash("sha256").update(fs.readFileSync(dest)).digest("hex");
190
+ if (actual !== expected) {
191
+ fs.rmSync(dest, { force: true });
192
+ throw new Error(
193
+ `checksum mismatch for ${path.basename(url)}\n` +
194
+ ` expected ${expected}\n actual ${actual}`
195
+ );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Resolve to a runnable `mcpbox` binary, downloading it when it is missing.
201
+ */
202
+ async function ensureBinary({ force = false, log = () => {} } = {}) {
203
+ if (process.env.MCPBOX_BINARY) {
204
+ if (!fs.existsSync(process.env.MCPBOX_BINARY)) {
205
+ throw new Error(
206
+ `MCPBOX_BINARY points at ${process.env.MCPBOX_BINARY}, which does not exist`
207
+ );
208
+ }
209
+ return process.env.MCPBOX_BINARY;
210
+ }
211
+
212
+ const target = targetName();
213
+ if (!target) {
214
+ throw new Error(
215
+ `mcpbox does not ship a binary for ${process.platform}/${process.arch}; ` +
216
+ `supported: ${SUPPORTED}. Build from source instead: ` +
217
+ `https://github.com/${repoSlug()}#build-from-source`
218
+ );
219
+ }
220
+
221
+ const dest = binaryPath(target);
222
+ const wanted = force || process.env.MCPBOX_FORCE_DOWNLOAD;
223
+ if (!wanted && isRunnable(dest)) return dest;
224
+
225
+ if (process.env.MCPBOX_SKIP_DOWNLOAD) {
226
+ if (isRunnable(dest)) return dest;
227
+ throw new Error(
228
+ `MCPBOX_SKIP_DOWNLOAD is set but no mcpbox binary is installed at ${dest}`
229
+ );
230
+ }
231
+
232
+ const urls = downloadUrls(target);
233
+ let lastError;
234
+ for (const url of urls) {
235
+ try {
236
+ log(`mcpbox: downloading ${url}`);
237
+ await downloadTo(url, dest);
238
+ await verify(dest, url, log);
239
+ log(`mcpbox: installed ${dest}`);
240
+ return dest;
241
+ } catch (err) {
242
+ lastError = err;
243
+ log(`mcpbox: ${err.message}`);
244
+ }
245
+ }
246
+
247
+ throw new Error(
248
+ `could not download the mcpbox binary for ${target}.\n` +
249
+ `Tried:\n${urls.map((u) => ` ${u}`).join("\n")}\n` +
250
+ `Last error: ${lastError && lastError.message}\n\n` +
251
+ `Install it another way instead:\n` +
252
+ ` curl -LsSf https://raw.githubusercontent.com/${repoSlug()}/master/install.sh | bash\n` +
253
+ `or set MCPBOX_DOWNLOAD_BASE to a mirror.`
254
+ );
255
+ }
256
+
257
+ module.exports = {
258
+ pkg,
259
+ TARGETS,
260
+ SUPPORTED,
261
+ targetName,
262
+ repoSlug,
263
+ version,
264
+ assetName,
265
+ downloadUrls,
266
+ cacheDir,
267
+ binaryPath,
268
+ isRunnable,
269
+ ensureBinary,
270
+ };
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@tomasko/mcpbox",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Carry Model Context Protocol tools into scripts, CLIs and agents — a self-contained MCP client in the spirit of mcporter.",
5
+ "license": "Apache-2.0",
6
+ "bin": {
7
+ "mcpbox": "bin/mcpbox.js"
8
+ },
9
+ "files": [
10
+ "bin/mcpbox.js",
11
+ "lib.js",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "os": [
18
+ "darwin",
19
+ "linux"
20
+ ],
21
+ "cpu": [
22
+ "x64",
23
+ "arm64"
24
+ ],
25
+ "scripts": {
26
+ "test": "node --test"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/tomasky/mcpbox.git"
31
+ },
32
+ "homepage": "https://github.com/tomasky/mcpbox",
33
+ "bugs": "https://github.com/tomasky/mcpbox/issues",
34
+ "keywords": [
35
+ "mcp",
36
+ "model-context-protocol",
37
+ "mcp-client",
38
+ "cli",
39
+ "agent",
40
+ "tools",
41
+ "mcporter",
42
+ "moonbit"
43
+ ],
44
+ "preferGlobal": true
45
+ }