@droidagentkit/launcher 0.2.0-alpha
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.js +231 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @droidagentkit/launcher
|
|
2
|
+
|
|
3
|
+
Thin Node launcher for the **DroidAgentKit** JVM MCP server.
|
|
4
|
+
|
|
5
|
+
- **Runtime:** pure JVM. Node is only an install-time shim.
|
|
6
|
+
- **Behavior:** on first run, downloads the `droidagent-cli-<version>.jar` matching this
|
|
7
|
+
package's version from [GitHub Releases](https://github.com/iVamsi/droid-agent-kit/releases),
|
|
8
|
+
verifies its SHA-256, caches it under `~/.droidagentkit/cli/`, and runs
|
|
9
|
+
`java -jar <jar> serve-mcp --transport stdio --project auto`. Later runs reuse the cached,
|
|
10
|
+
already-verified jar without hitting the network again.
|
|
11
|
+
- **Version flag:** `droidagent-mcp --version` prints immutable launcher/server version metadata.
|
|
12
|
+
- **Requires:** a JDK 17+ runtime on `PATH` (the same prerequisite as building from source).
|
|
13
|
+
- **Overrides:**
|
|
14
|
+
- `DROIDAGENT_BIN` — absolute path to an existing `droidagent` CLI; skips auto-fetch entirely.
|
|
15
|
+
- `DROIDAGENT_CACHE_DIR` — change where the downloaded jar is cached.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx -y @droidagentkit/launcher --version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
or install it once:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @droidagentkit/launcher
|
|
27
|
+
droidagent-mcp --version
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Fails closed
|
|
31
|
+
|
|
32
|
+
A checksum mismatch, a failed download, or a missing `java` on `PATH` all exit non-zero with a
|
|
33
|
+
clear message instead of silently degrading the MCP connection or running unverified code.
|
|
34
|
+
|
|
35
|
+
## Smoke test
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
../smoke-test.sh # exercises --version/--help, the auto-fetch/cache/checksum paths, and
|
|
39
|
+
# (with DROIDAGENT_E2E=1) a real stdio round-trip against a built CLI
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
See `docs/adrs/0001-packaging.md` for the packaging decision (npm launcher primary,
|
|
43
|
+
MCPB secondary, OCI rejected) and the release pipeline that publishes this package.
|
package/index.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* DroidAgentKit MCP launcher.
|
|
4
|
+
*
|
|
5
|
+
* Default behavior: downloads (once, then caches) the `droidagent-cli` fat jar matching this
|
|
6
|
+
* launcher's own version from GitHub Releases, verifies its SHA-256, and runs it with
|
|
7
|
+
* `java -jar ... serve-mcp --transport stdio --project auto`.
|
|
8
|
+
*
|
|
9
|
+
* Set DROIDAGENT_BIN to an existing `droidagent` CLI to skip auto-fetch entirely (e.g. a local
|
|
10
|
+
* `./gradlew :cli:installDist` build). Node is an install-time shim only; the MCP server itself
|
|
11
|
+
* is pure JVM and has no Node runtime dependency. The launcher fails closed on any error rather
|
|
12
|
+
* than silently degrading the MCP connection.
|
|
13
|
+
*/
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
17
|
+
const { existsSync, mkdirSync, renameSync, unlinkSync, createWriteStream, readFileSync } = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
const os = require("node:os");
|
|
20
|
+
const http = require("node:http");
|
|
21
|
+
const https = require("node:https");
|
|
22
|
+
const crypto = require("node:crypto");
|
|
23
|
+
|
|
24
|
+
const LAUNCHER_VERSION = require("./package.json").version;
|
|
25
|
+
|
|
26
|
+
const GITHUB_OWNER = "iVamsi";
|
|
27
|
+
const GITHUB_REPO = "droid-agent-kit";
|
|
28
|
+
const DEFAULT_RELEASE_BASE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download`;
|
|
29
|
+
const MAX_REDIRECTS = 5;
|
|
30
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
31
|
+
|
|
32
|
+
function cacheDir() {
|
|
33
|
+
return process.env.DROIDAGENT_CACHE_DIR || path.join(os.homedir(), ".droidagentkit", "cli");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function jarFileName(version) {
|
|
37
|
+
return `droidagent-cli-${version}.jar`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Overridable only for tests (see smoke-test.sh); production always uses the real GitHub
|
|
41
|
+
// Releases URL for this repo.
|
|
42
|
+
function releaseAssetUrl(version, fileName) {
|
|
43
|
+
const base = process.env.DROIDAGENT_RELEASE_BASE_URL || DEFAULT_RELEASE_BASE_URL;
|
|
44
|
+
return `${base}/v${version}/${fileName}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function httpModuleFor(url) {
|
|
48
|
+
return new URL(url).protocol === "http:" ? http : https;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fetchBuffer(url, redirectsLeft = MAX_REDIRECTS) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const req = httpModuleFor(url).get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
54
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirectsLeft > 0) {
|
|
55
|
+
res.resume();
|
|
56
|
+
resolve(fetchBuffer(res.headers.location, redirectsLeft - 1));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (res.statusCode !== 200) {
|
|
60
|
+
res.resume();
|
|
61
|
+
reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const chunks = [];
|
|
65
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
66
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
67
|
+
res.on("error", reject);
|
|
68
|
+
});
|
|
69
|
+
req.on("timeout", () => req.destroy(new Error(`timed out fetching ${url}`)));
|
|
70
|
+
req.on("error", reject);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function downloadToFile(url, destPath, redirectsLeft = MAX_REDIRECTS) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const req = httpModuleFor(url).get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
77
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirectsLeft > 0) {
|
|
78
|
+
res.resume();
|
|
79
|
+
resolve(downloadToFile(res.headers.location, destPath, redirectsLeft - 1));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (res.statusCode !== 200) {
|
|
83
|
+
res.resume();
|
|
84
|
+
reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const out = createWriteStream(destPath);
|
|
88
|
+
res.pipe(out);
|
|
89
|
+
out.on("finish", () => out.close(() => resolve()));
|
|
90
|
+
out.on("error", reject);
|
|
91
|
+
res.on("error", reject);
|
|
92
|
+
});
|
|
93
|
+
req.on("timeout", () => req.destroy(new Error(`timed out fetching ${url}`)));
|
|
94
|
+
req.on("error", reject);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sha256File(filePath) {
|
|
99
|
+
return crypto.createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseChecksum(buffer) {
|
|
103
|
+
const text = buffer.toString("utf8").trim();
|
|
104
|
+
const hash = text.split(/\s+/)[0] ?? "";
|
|
105
|
+
if (!/^[0-9a-f]{64}$/i.test(hash)) {
|
|
106
|
+
throw new Error(`unrecognized checksum file format: ${text.slice(0, 80)}`);
|
|
107
|
+
}
|
|
108
|
+
return hash.toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Downloads and verifies the droidagent-cli fat jar for `version` into the cache dir, or
|
|
112
|
+
// reuses it if already present. Fails closed: a checksum mismatch deletes the partial download
|
|
113
|
+
// and throws rather than falling back to an unverified jar. Verification only happens on a
|
|
114
|
+
// fresh download; a cache hit is trusted on the strength of that earlier verification, so a
|
|
115
|
+
// GitHub outage doesn't block an already-working install.
|
|
116
|
+
async function ensureCliJar(version) {
|
|
117
|
+
const dir = cacheDir();
|
|
118
|
+
const finalPath = path.join(dir, jarFileName(version));
|
|
119
|
+
if (existsSync(finalPath)) {
|
|
120
|
+
return finalPath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
mkdirSync(dir, { recursive: true });
|
|
124
|
+
const fileName = jarFileName(version);
|
|
125
|
+
const jarUrl = releaseAssetUrl(version, fileName);
|
|
126
|
+
const checksumUrl = `${jarUrl}.sha256`;
|
|
127
|
+
const tmpPath = path.join(dir, `.${fileName}.${process.pid}.tmp`);
|
|
128
|
+
|
|
129
|
+
let expectedHash;
|
|
130
|
+
try {
|
|
131
|
+
expectedHash = parseChecksum(await fetchBuffer(checksumUrl));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
throw new Error(`could not fetch checksum from ${checksumUrl}: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await downloadToFile(jarUrl, tmpPath);
|
|
138
|
+
const actualHash = sha256File(tmpPath);
|
|
139
|
+
if (actualHash !== expectedHash) {
|
|
140
|
+
throw new Error(`checksum mismatch: expected ${expectedHash}, got ${actualHash}`);
|
|
141
|
+
}
|
|
142
|
+
renameSync(tmpPath, finalPath);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
return finalPath;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function javaAvailable() {
|
|
151
|
+
return !spawnSync("java", ["-version"]).error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function spawnServer(command, args) {
|
|
155
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
156
|
+
child.on("error", (err) => {
|
|
157
|
+
process.stderr.write(`droidagent-mcp: failed to launch '${command}': ${err.message}\n`);
|
|
158
|
+
process.exit(127);
|
|
159
|
+
});
|
|
160
|
+
child.on("exit", (code, signal) => {
|
|
161
|
+
if (signal) process.exit(1);
|
|
162
|
+
process.exit(code ?? 1);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function printHelp() {
|
|
167
|
+
process.stdout.write(
|
|
168
|
+
"Usage: droidagent-mcp [--version | --help]\n" +
|
|
169
|
+
" (no args) fetch (if needed) and run the DroidAgentKit MCP server over stdio\n" +
|
|
170
|
+
"\n" +
|
|
171
|
+
"Env:\n" +
|
|
172
|
+
" DROIDAGENT_BIN absolute path to an existing droidagent CLI (skips auto-fetch)\n" +
|
|
173
|
+
" DROIDAGENT_CACHE_DIR override the jar download cache (default ~/.droidagentkit/cli)\n"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveOverrideBin() {
|
|
178
|
+
if (process.env.DROIDAGENT_BIN && existsSync(process.env.DROIDAGENT_BIN)) {
|
|
179
|
+
return process.env.DROIDAGENT_BIN;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function main(argv) {
|
|
185
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
186
|
+
process.stdout.write(`droidagent-mcp launcher ${LAUNCHER_VERSION}\n`);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
190
|
+
printHelp();
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const overrideBin = resolveOverrideBin();
|
|
195
|
+
if (overrideBin) {
|
|
196
|
+
spawnServer(overrideBin, ["serve-mcp", "--transport", "stdio", "--project", "auto"]);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!javaAvailable()) {
|
|
201
|
+
process.stderr.write(
|
|
202
|
+
"droidagent-mcp: no working `java` found on PATH.\n" +
|
|
203
|
+
"DroidAgentKit's server is a JVM tool and needs a JDK 17+ runtime (e.g. https://adoptium.net).\n" +
|
|
204
|
+
"Alternatively, set DROIDAGENT_BIN to a prebuilt droidagent CLI.\n"
|
|
205
|
+
);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let jarPath;
|
|
210
|
+
try {
|
|
211
|
+
jarPath = await ensureCliJar(LAUNCHER_VERSION);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
process.stderr.write(
|
|
214
|
+
`droidagent-mcp: could not fetch droidagent-cli ${LAUNCHER_VERSION}: ${err.message}\n` +
|
|
215
|
+
"Set DROIDAGENT_BIN to a prebuilt droidagent CLI to bypass auto-fetch.\n"
|
|
216
|
+
);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
spawnServer("java", ["-jar", jarPath, "serve-mcp", "--transport", "stdio", "--project", "auto"]);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main(process.argv.slice(2))
|
|
225
|
+
.then((code) => {
|
|
226
|
+
if (code !== null && code !== undefined) process.exit(code);
|
|
227
|
+
})
|
|
228
|
+
.catch((err) => {
|
|
229
|
+
process.stderr.write(`droidagent-mcp: unexpected error: ${err.message}\n`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@droidagentkit/launcher",
|
|
3
|
+
"version": "0.2.0-alpha",
|
|
4
|
+
"description": "Thin Node launcher for the DroidAgentKit JVM MCP server. Locates the `droidagent` CLI (or a fetched archive), verifies its SHA-256, and spawns `droidagent serve-mcp --transport stdio`. Node is an install-time dependency only; the server runtime is pure JVM.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/iVamsi/droid-agent-kit.git",
|
|
9
|
+
"directory": "distribution/npm-launcher"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/iVamsi/droid-agent-kit",
|
|
12
|
+
"bugs": "https://github.com/iVamsi/droid-agent-kit/issues",
|
|
13
|
+
"bin": {
|
|
14
|
+
"droidagent-mcp": "index.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.js",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"smoke": "node index.js --version"
|
|
25
|
+
}
|
|
26
|
+
}
|