@helmdeck/mcp-bridge 0.3.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 +49 -0
- package/bin/helmdeck-mcp.js +34 -0
- package/package.json +38 -0
- package/scripts/postinstall.js +147 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @helmdeck/mcp-bridge
|
|
2
|
+
|
|
3
|
+
Stdio MCP bridge for [helmdeck](https://github.com/tosin2013/helmdeck).
|
|
4
|
+
Connects Claude Code, Claude Desktop, OpenClaw, and Gemini CLI to a
|
|
5
|
+
helmdeck control plane via the [Model Context Protocol](https://modelcontextprotocol.io).
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
# One-shot, no install
|
|
11
|
+
HELMDECK_URL=https://helmdeck.example HELMDECK_TOKEN=... npx @helmdeck/mcp-bridge
|
|
12
|
+
|
|
13
|
+
# Or install globally
|
|
14
|
+
npm install -g @helmdeck/mcp-bridge
|
|
15
|
+
helmdeck-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
The bridge reads two environment variables:
|
|
21
|
+
|
|
22
|
+
| Variable | Description |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `HELMDECK_URL` | Base URL of the helmdeck control plane (e.g. `https://helmdeck.example`) |
|
|
25
|
+
| `HELMDECK_TOKEN` | Bearer JWT issued from the Management UI's API Tokens panel |
|
|
26
|
+
|
|
27
|
+
## Client snippets
|
|
28
|
+
|
|
29
|
+
The control plane exposes ready-to-paste configuration snippets at
|
|
30
|
+
`GET /api/v1/connect/{client}` for each supported client. Example:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
curl -H "Authorization: Bearer $HELMDECK_TOKEN" \
|
|
34
|
+
"$HELMDECK_URL/api/v1/connect/claude-code"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Supported clients: `claude-code`, `claude-desktop`, `openclaw`, `gemini-cli`.
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
`postinstall` downloads the platform-matching `helmdeck-mcp` binary
|
|
42
|
+
from the corresponding GitHub Release, verifies its SHA256 against
|
|
43
|
+
`checksums.txt`, and places it in `bin/`. Set
|
|
44
|
+
`HELMDECK_MCP_SKIP_DOWNLOAD=1` to skip the download (useful in CI
|
|
45
|
+
images that bake the binary in another way).
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
Apache-2.0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launcher shim for @helmdeck/mcp-bridge.
|
|
3
|
+
//
|
|
4
|
+
// npm sets up the `helmdeck-mcp` bin symlink at install time, *before*
|
|
5
|
+
// postinstall runs, so this file must exist in the published tarball.
|
|
6
|
+
// At runtime it execs the platform-native binary that postinstall
|
|
7
|
+
// downloaded into the same directory; if the binary is missing it
|
|
8
|
+
// prints a clear remediation hint instead of a cryptic spawn error.
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const { spawnSync } = require("child_process");
|
|
15
|
+
|
|
16
|
+
const binName = process.platform === "win32" ? "helmdeck-mcp.exe" : "helmdeck-mcp";
|
|
17
|
+
const binPath = path.join(__dirname, binName);
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(binPath)) {
|
|
20
|
+
console.error(
|
|
21
|
+
`[helmdeck-mcp] native binary not found at ${binPath}.\n` +
|
|
22
|
+
`This usually means the postinstall step was skipped (HELMDECK_MCP_SKIP_DOWNLOAD=1)\n` +
|
|
23
|
+
`or the download failed. Re-run:\n\n` +
|
|
24
|
+
` npm rebuild @helmdeck/mcp-bridge\n`
|
|
25
|
+
);
|
|
26
|
+
process.exit(127);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = spawnSync(binPath, process.argv.slice(2), { stdio: "inherit" });
|
|
30
|
+
if (result.error) {
|
|
31
|
+
console.error(`[helmdeck-mcp] failed to spawn ${binPath}: ${result.error.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@helmdeck/mcp-bridge",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Stdio MCP bridge that connects Claude Code, Claude Desktop, OpenClaw, and Gemini CLI to a helmdeck control plane.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"helmdeck-mcp": "bin/helmdeck-mcp.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"scripts/",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"postinstall": "node scripts/postinstall.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/tosin2013/helmdeck.git",
|
|
22
|
+
"directory": "npm"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/tosin2013/helmdeck",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/tosin2013/helmdeck/issues"
|
|
27
|
+
},
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"model-context-protocol",
|
|
32
|
+
"helmdeck",
|
|
33
|
+
"claude",
|
|
34
|
+
"gemini",
|
|
35
|
+
"agent",
|
|
36
|
+
"bridge"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// postinstall: download the helmdeck-mcp binary matching the host's
|
|
3
|
+
// platform/arch from the GitHub release that corresponds to this
|
|
4
|
+
// package's version, verify its checksum against checksums.txt, and
|
|
5
|
+
// drop it into bin/ next to the launcher shim.
|
|
6
|
+
//
|
|
7
|
+
// Skipped automatically when HELMDECK_MCP_SKIP_DOWNLOAD=1 (so CI
|
|
8
|
+
// images that bake the binary in another way don't double-fetch).
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const https = require("https");
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
const zlib = require("zlib");
|
|
17
|
+
const { execSync } = require("child_process");
|
|
18
|
+
|
|
19
|
+
if (process.env.HELMDECK_MCP_SKIP_DOWNLOAD === "1") {
|
|
20
|
+
console.log("[helmdeck-mcp] HELMDECK_MCP_SKIP_DOWNLOAD=1, skipping binary download");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pkg = require("../package.json");
|
|
25
|
+
const VERSION = `v${pkg.version}`;
|
|
26
|
+
const REPO = "tosin2013/helmdeck";
|
|
27
|
+
|
|
28
|
+
const PLATFORM_MAP = {
|
|
29
|
+
linux: "linux",
|
|
30
|
+
darwin: "darwin",
|
|
31
|
+
win32: "windows",
|
|
32
|
+
};
|
|
33
|
+
const ARCH_MAP = {
|
|
34
|
+
x64: "amd64",
|
|
35
|
+
arm64: "arm64",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const os = PLATFORM_MAP[process.platform];
|
|
39
|
+
const arch = ARCH_MAP[process.arch];
|
|
40
|
+
if (!os || !arch) {
|
|
41
|
+
console.error(`[helmdeck-mcp] unsupported platform ${process.platform}/${process.arch}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ext = os === "windows" ? "zip" : "tar.gz";
|
|
46
|
+
const archiveName = `helmdeck-mcp_${pkg.version}_${os}_${arch}.${ext}`;
|
|
47
|
+
const baseURL = `https://github.com/${REPO}/releases/download/${VERSION}`;
|
|
48
|
+
const archiveURL = `${baseURL}/${archiveName}`;
|
|
49
|
+
const checksumsURL = `${baseURL}/checksums.txt`;
|
|
50
|
+
|
|
51
|
+
const binDir = path.join(__dirname, "..", "bin");
|
|
52
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
function get(url) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
https
|
|
57
|
+
.get(url, { headers: { "User-Agent": "helmdeck-mcp-postinstall" } }, (res) => {
|
|
58
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
59
|
+
resolve(get(res.headers.location));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (res.statusCode !== 200) {
|
|
63
|
+
reject(new Error(`GET ${url} -> ${res.statusCode}`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const chunks = [];
|
|
67
|
+
res.on("data", (c) => chunks.push(c));
|
|
68
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
69
|
+
res.on("error", reject);
|
|
70
|
+
})
|
|
71
|
+
.on("error", reject);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
(async () => {
|
|
76
|
+
try {
|
|
77
|
+
console.log(`[helmdeck-mcp] downloading ${archiveURL}`);
|
|
78
|
+
const archive = await get(archiveURL);
|
|
79
|
+
|
|
80
|
+
console.log(`[helmdeck-mcp] verifying checksum`);
|
|
81
|
+
const checksums = (await get(checksumsURL)).toString("utf8");
|
|
82
|
+
const wantLine = checksums.split("\n").find((l) => l.endsWith(" " + archiveName));
|
|
83
|
+
if (!wantLine) {
|
|
84
|
+
throw new Error(`checksum entry for ${archiveName} not found in checksums.txt`);
|
|
85
|
+
}
|
|
86
|
+
const want = wantLine.split(/\s+/)[0];
|
|
87
|
+
const got = crypto.createHash("sha256").update(archive).digest("hex");
|
|
88
|
+
if (got !== want) {
|
|
89
|
+
throw new Error(`checksum mismatch: want ${want}, got ${got}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const archivePath = path.join(binDir, archiveName);
|
|
93
|
+
fs.writeFileSync(archivePath, archive);
|
|
94
|
+
|
|
95
|
+
console.log(`[helmdeck-mcp] extracting`);
|
|
96
|
+
if (ext === "zip") {
|
|
97
|
+
// Defer to a built-in unzip on each OS rather than ship a JS unzip dep.
|
|
98
|
+
execSync(`powershell -Command "Expand-Archive -Force '${archivePath}' '${binDir}'"`, {
|
|
99
|
+
stdio: "inherit",
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
// Stream-decompress + untar inline so we don't shell out on Linux/Darwin.
|
|
103
|
+
const tar = zlib.gunzipSync(archive);
|
|
104
|
+
extractTar(tar, binDir);
|
|
105
|
+
}
|
|
106
|
+
fs.unlinkSync(archivePath);
|
|
107
|
+
|
|
108
|
+
const binName = os === "windows" ? "helmdeck-mcp.exe" : "helmdeck-mcp";
|
|
109
|
+
const binPath = path.join(binDir, binName);
|
|
110
|
+
if (!fs.existsSync(binPath)) {
|
|
111
|
+
throw new Error(`expected ${binPath} after extraction`);
|
|
112
|
+
}
|
|
113
|
+
if (os !== "windows") {
|
|
114
|
+
fs.chmodSync(binPath, 0o755);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// bin/helmdeck-mcp.js is shipped in the tarball — npm has
|
|
118
|
+
// already symlinked it as the package's bin entry by the time
|
|
119
|
+
// postinstall runs, so we only need to drop the native binary
|
|
120
|
+
// next to it.
|
|
121
|
+
console.log(`[helmdeck-mcp] installed ${binPath}`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`[helmdeck-mcp] postinstall failed: ${err.message}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
|
|
128
|
+
// Minimal POSIX-tar extractor: enough for goreleaser archives, which
|
|
129
|
+
// only contain regular files. Does not handle long-name extensions
|
|
130
|
+
// because goreleaser doesn't emit them for our binary names.
|
|
131
|
+
function extractTar(buf, dest) {
|
|
132
|
+
let offset = 0;
|
|
133
|
+
while (offset + 512 <= buf.length) {
|
|
134
|
+
const header = buf.slice(offset, offset + 512);
|
|
135
|
+
if (header[0] === 0) break;
|
|
136
|
+
const name = header.slice(0, 100).toString("utf8").replace(/\0.*$/, "");
|
|
137
|
+
const sizeStr = header.slice(124, 136).toString("utf8").replace(/\0.*$/, "").trim();
|
|
138
|
+
const size = parseInt(sizeStr, 8);
|
|
139
|
+
const type = header[156];
|
|
140
|
+
offset += 512;
|
|
141
|
+
if ((type === 0 || type === 0x30) && name) {
|
|
142
|
+
const out = path.join(dest, path.basename(name));
|
|
143
|
+
fs.writeFileSync(out, buf.slice(offset, offset + size));
|
|
144
|
+
}
|
|
145
|
+
offset += Math.ceil(size / 512) * 512;
|
|
146
|
+
}
|
|
147
|
+
}
|