@kinlab/kin-mcp 0.2.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 ADDED
@@ -0,0 +1,72 @@
1
+ # @kinlab/kin-mcp
2
+
3
+ `@kinlab/kin-mcp` is the npm-friendly launcher for Kin's MCP server (the installed
4
+ command is still `kin-mcp`).
5
+
6
+ It downloads the matching Kin release archive from GitHub, verifies the published
7
+ SHA-256 checksum, extracts the `kin` binary into a local cache, and runs:
8
+
9
+ ```sh
10
+ kin mcp start
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Use it directly from an MCP client with `npx`:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "kin": {
21
+ "command": "npx",
22
+ "args": ["-y", "@kinlab/kin-mcp"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ For a pinned version:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "kin": {
34
+ "command": "npx",
35
+ "args": ["-y", "@kinlab/kin-mcp@0.2.0"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Requirements
42
+
43
+ - Node.js 20+
44
+ - macOS or Linux
45
+ - A Kin-initialized repository (`kin init`)
46
+
47
+ Windows users should run Kin through WSL2 during the alpha.
48
+
49
+ ## Cache
50
+
51
+ The wrapper caches the downloaded `kin` binary under:
52
+
53
+ - macOS: `~/Library/Caches/kin-mcp`
54
+ - Linux: `~/.cache/kin-mcp`
55
+
56
+ Set `KIN_MCP_CACHE_DIR` to override the cache location.
57
+
58
+ ## Environment
59
+
60
+ - `KIN_MCP_KIN_BINARY`: run a specific local `kin` binary instead of downloading one
61
+ - `KIN_BINARY_PATH`: alias for `KIN_MCP_KIN_BINARY`
62
+ - `KIN_MCP_CACHE_DIR`: override the cache directory
63
+ - `KIN_MCP_AUTO_INIT=1`: allow the wrapper to run `kin init .` when `.kin/` is missing
64
+ - `KIN_MCP_RELEASE_BASE_URL`: override the GitHub release download base URL
65
+
66
+ ## Local Check
67
+
68
+ ```sh
69
+ npx -y @kinlab/kin-mcp --print-bin
70
+ ```
71
+
72
+ Then initialize a repository and let the MCP client launch `kin-mcp`.
package/bin/kin-mcp.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runKinMcp } from '../src/index.js';
4
+
5
+ const exitCode = await runKinMcp(process.argv.slice(2));
6
+ process.exit(exitCode);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@kinlab/kin-mcp",
3
+ "version": "0.2.0",
4
+ "description": "Start Kin's MCP server through an npm-friendly wrapper.",
5
+ "type": "module",
6
+ "bin": {
7
+ "kin-mcp": "./bin/kin-mcp.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md",
13
+ "package.json"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test",
17
+ "lint": "node --check ./bin/kin-mcp.js && node --check ./src/index.js && node --check ./test/index.test.js"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "keywords": [
23
+ "kin",
24
+ "mcp",
25
+ "claude",
26
+ "codex",
27
+ "gemini"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/firelock-ai/kin.git"
32
+ },
33
+ "homepage": "https://github.com/firelock-ai/kin",
34
+ "bugs": {
35
+ "url": "https://github.com/firelock-ai/kin/issues"
36
+ },
37
+ "license": "Apache-2.0",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }
package/src/index.js ADDED
@@ -0,0 +1,385 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Firelock, LLC
3
+
4
+ import cp from 'node:child_process';
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import fsp from 'node:fs/promises';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { createRequire } from 'node:module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const packageJson = require('../package.json');
14
+
15
+ export const PACKAGE_VERSION = packageJson.version;
16
+ export const DEFAULT_RELEASE_BASE_URL =
17
+ 'https://github.com/firelock-ai/kin/releases/download';
18
+
19
+ export function resolveReleaseTag(version = PACKAGE_VERSION) {
20
+ return version.startsWith('v') ? version : `v${version}`;
21
+ }
22
+
23
+ export function resolveReleaseAsset(platform = process.platform, arch = process.arch) {
24
+ if (platform === 'darwin' && arch === 'arm64') {
25
+ return {
26
+ assetName: 'kin-macos-aarch64',
27
+ archiveName: 'kin-macos-aarch64.tar.gz',
28
+ binaryName: 'kin'
29
+ };
30
+ }
31
+ if (platform === 'darwin' && arch === 'x64') {
32
+ return {
33
+ assetName: 'kin-macos-x86_64',
34
+ archiveName: 'kin-macos-x86_64.tar.gz',
35
+ binaryName: 'kin'
36
+ };
37
+ }
38
+ if (platform === 'linux' && arch === 'x64') {
39
+ return {
40
+ assetName: 'kin-linux-x86_64',
41
+ archiveName: 'kin-linux-x86_64.tar.gz',
42
+ binaryName: 'kin'
43
+ };
44
+ }
45
+ if (platform === 'linux' && arch === 'arm64') {
46
+ return {
47
+ assetName: 'kin-linux-aarch64',
48
+ archiveName: 'kin-linux-aarch64.tar.gz',
49
+ binaryName: 'kin'
50
+ };
51
+ }
52
+
53
+ throw new Error(
54
+ `kin-mcp does not have a published Kin binary for ${platform}/${arch} yet. ` +
55
+ 'On Windows, use WSL2 for this alpha.'
56
+ );
57
+ }
58
+
59
+ export function resolveCacheRoot(
60
+ env = process.env,
61
+ platform = process.platform,
62
+ homeDir = os.homedir()
63
+ ) {
64
+ if (env.KIN_MCP_CACHE_DIR) {
65
+ return path.resolve(env.KIN_MCP_CACHE_DIR);
66
+ }
67
+
68
+ if (platform === 'darwin') {
69
+ return path.join(homeDir, 'Library', 'Caches', 'kin-mcp');
70
+ }
71
+
72
+ if (platform === 'win32') {
73
+ const localAppData = env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
74
+ return path.join(localAppData, 'kin-mcp', 'Cache');
75
+ }
76
+
77
+ const xdgCacheHome = env.XDG_CACHE_HOME || path.join(homeDir, '.cache');
78
+ return path.join(xdgCacheHome, 'kin-mcp');
79
+ }
80
+
81
+ export function resolveCachedBinaryPath({
82
+ env = process.env,
83
+ platform = process.platform,
84
+ arch = process.arch,
85
+ version = PACKAGE_VERSION,
86
+ homeDir = os.homedir(),
87
+ cacheRoot
88
+ } = {}) {
89
+ const { assetName, binaryName } = resolveReleaseAsset(platform, arch);
90
+ const root = cacheRoot || resolveCacheRoot(env, platform, homeDir);
91
+ return path.join(root, resolveReleaseTag(version), assetName, binaryName);
92
+ }
93
+
94
+ export async function ensureKinBinary({
95
+ env = process.env,
96
+ platform = process.platform,
97
+ arch = process.arch,
98
+ version = PACKAGE_VERSION,
99
+ homeDir = os.homedir(),
100
+ cacheRoot
101
+ } = {}) {
102
+ const configuredBinary = env.KIN_MCP_KIN_BINARY || env.KIN_BINARY_PATH;
103
+ if (configuredBinary) {
104
+ const resolved = path.resolve(configuredBinary);
105
+ await assertRunnable(resolved, platform);
106
+ return resolved;
107
+ }
108
+
109
+ const binaryPath = resolveCachedBinaryPath({
110
+ env,
111
+ platform,
112
+ arch,
113
+ version,
114
+ homeDir,
115
+ cacheRoot
116
+ });
117
+
118
+ if (await isRunnable(binaryPath, platform)) {
119
+ return binaryPath;
120
+ }
121
+
122
+ await installKinBinary({ binaryPath, env, platform, arch, version });
123
+ return binaryPath;
124
+ }
125
+
126
+ export async function runKinMcp(argv = [], options = {}) {
127
+ const stdout = options.stdout || process.stdout;
128
+ const stderr = options.stderr || process.stderr;
129
+ const env = options.env || process.env;
130
+
131
+ if (argv.includes('--help') || argv.includes('-h')) {
132
+ stdout.write(renderHelp());
133
+ return 0;
134
+ }
135
+
136
+ if (argv.includes('--version') || argv.includes('-v')) {
137
+ stdout.write(`${PACKAGE_VERSION}\n`);
138
+ return 0;
139
+ }
140
+
141
+ if (argv.includes('--print-bin')) {
142
+ const binaryPath = await ensureKinBinary(options);
143
+ stdout.write(`${binaryPath}\n`);
144
+ return 0;
145
+ }
146
+
147
+ if (argv.length > 0) {
148
+ stderr.write(
149
+ 'kin-mcp does not accept subcommands. It always runs `kin mcp start`.\n'
150
+ );
151
+ return 2;
152
+ }
153
+
154
+ const binaryPath = await ensureKinBinary(options);
155
+
156
+ const cwd = options.cwd || process.cwd();
157
+ if (!await kinRepoExists(cwd)) {
158
+ if (!isTruthyEnv(env.KIN_MCP_AUTO_INIT)) {
159
+ stderr.write(
160
+ 'No .kin/ found. Run `kin init .` first, or set KIN_MCP_AUTO_INIT=1 to allow this wrapper to initialize the repo.\n'
161
+ );
162
+ return 2;
163
+ }
164
+ stderr.write('No .kin/ found; KIN_MCP_AUTO_INIT=1, running kin init...\n');
165
+ const initCode = await spawnKin(binaryPath, ['init', '.'], { ...options, cwd });
166
+ if (initCode !== 0) {
167
+ stderr.write('kin init failed. Cannot start MCP server.\n');
168
+ return initCode;
169
+ }
170
+ }
171
+
172
+ return spawnKin(binaryPath, ['mcp', 'start'], options);
173
+ }
174
+
175
+ function isTruthyEnv(value) {
176
+ return ['1', 'true', 'TRUE', 'yes', 'YES'].includes(String(value || ''));
177
+ }
178
+
179
+ function renderHelp() {
180
+ return `kin-mcp ${PACKAGE_VERSION}
181
+
182
+ Usage:
183
+ kin-mcp
184
+ kin-mcp --print-bin
185
+ kin-mcp --version
186
+
187
+ This wrapper downloads a matching Kin release binary on demand, caches it
188
+ locally, and then runs:
189
+
190
+ kin mcp start
191
+
192
+ Environment:
193
+ KIN_MCP_KIN_BINARY Use a specific kin binary
194
+ KIN_BINARY_PATH Alias for KIN_MCP_KIN_BINARY
195
+ KIN_MCP_CACHE_DIR Override the cache directory
196
+ KIN_MCP_AUTO_INIT Set to 1 to allow wrapper-initiated kin init
197
+ KIN_MCP_RELEASE_BASE_URL
198
+ Override the release download base URL
199
+ `;
200
+ }
201
+
202
+ async function installKinBinary({ binaryPath, env, platform, arch, version }) {
203
+ const { assetName, archiveName, binaryName } = resolveReleaseAsset(platform, arch);
204
+ const tag = resolveReleaseTag(version);
205
+ const baseUrl = (env.KIN_MCP_RELEASE_BASE_URL || DEFAULT_RELEASE_BASE_URL).replace(
206
+ /\/$/,
207
+ ''
208
+ );
209
+ const archiveUrl = `${baseUrl}/${tag}/${archiveName}`;
210
+ const checksumUrl = `${archiveUrl}.sha256`;
211
+
212
+ await fsp.mkdir(path.dirname(binaryPath), { recursive: true });
213
+
214
+ const checksumText = await fetchText(checksumUrl);
215
+ const expectedSha = parseChecksum(checksumText);
216
+ const archiveBytes = await fetchBytes(archiveUrl);
217
+ const actualSha = sha256(archiveBytes);
218
+
219
+ if (actualSha !== expectedSha) {
220
+ throw new Error(
221
+ `checksum mismatch for ${archiveName}: expected ${expectedSha}, got ${actualSha}`
222
+ );
223
+ }
224
+
225
+ await installFromArchive({
226
+ archiveBytes,
227
+ archiveName,
228
+ assetName,
229
+ binaryName,
230
+ binaryPath,
231
+ platform
232
+ });
233
+ }
234
+
235
+ async function installFromArchive({
236
+ archiveBytes,
237
+ archiveName,
238
+ assetName,
239
+ binaryName,
240
+ binaryPath,
241
+ platform
242
+ }) {
243
+ const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'kin-mcp-install-'));
244
+ const archivePath = path.join(tmpRoot, archiveName);
245
+ const tmpPath = `${binaryPath}.download`;
246
+
247
+ try {
248
+ await fsp.writeFile(archivePath, archiveBytes);
249
+ await execFile('tar', ['-xzf', archivePath, '-C', tmpRoot]);
250
+
251
+ const extractedBinary = path.join(tmpRoot, assetName, binaryName);
252
+ await fsp.access(extractedBinary, fs.constants.R_OK);
253
+ await fsp.copyFile(extractedBinary, tmpPath);
254
+ if (platform !== 'win32') {
255
+ await fsp.chmod(tmpPath, 0o755);
256
+ }
257
+ await fsp.rename(tmpPath, binaryPath);
258
+ } catch (error) {
259
+ await fsp.unlink(tmpPath).catch(() => {});
260
+ throw new Error(`failed to install ${binaryName} from ${archiveName}: ${error.message}`);
261
+ } finally {
262
+ await fsp.rm(tmpRoot, { recursive: true, force: true });
263
+ }
264
+ }
265
+
266
+ function execFile(file, args, options = {}) {
267
+ return new Promise((resolve, reject) => {
268
+ cp.execFile(file, args, options, (error, stdout, stderr) => {
269
+ if (error) {
270
+ if (stderr) {
271
+ error.message = `${error.message}: ${stderr.trim()}`;
272
+ }
273
+ reject(error);
274
+ return;
275
+ }
276
+ resolve({ stdout, stderr });
277
+ });
278
+ });
279
+ }
280
+
281
+ async function fetchText(url) {
282
+ const response = await fetch(url, {
283
+ headers: { 'user-agent': `kin-mcp/${PACKAGE_VERSION}` },
284
+ signal: AbortSignal.timeout(60_000)
285
+ });
286
+
287
+ if (!response.ok) {
288
+ throw new Error(`failed to download ${url}: ${response.status} ${response.statusText}`);
289
+ }
290
+
291
+ return response.text();
292
+ }
293
+
294
+ async function fetchBytes(url) {
295
+ const response = await fetch(url, {
296
+ headers: { 'user-agent': `kin-mcp/${PACKAGE_VERSION}` },
297
+ signal: AbortSignal.timeout(120_000)
298
+ });
299
+
300
+ if (!response.ok) {
301
+ throw new Error(`failed to download ${url}: ${response.status} ${response.statusText}`);
302
+ }
303
+
304
+ return Buffer.from(await response.arrayBuffer());
305
+ }
306
+
307
+ function parseChecksum(text) {
308
+ const match = text.trim().match(/\b([a-fA-F0-9]{64})\b/);
309
+ if (!match) {
310
+ throw new Error('failed to parse SHA256 checksum from release metadata');
311
+ }
312
+ return match[1].toLowerCase();
313
+ }
314
+
315
+ function sha256(bytes) {
316
+ return crypto.createHash('sha256').update(bytes).digest('hex');
317
+ }
318
+
319
+ async function kinRepoExists(cwd) {
320
+ try {
321
+ const stat = await fsp.stat(path.join(cwd, '.kin'));
322
+ return stat.isDirectory();
323
+ } catch {
324
+ return false;
325
+ }
326
+ }
327
+
328
+ async function assertRunnable(filePath, platform) {
329
+ if (!(await isRunnable(filePath, platform))) {
330
+ throw new Error(`kin binary not found or not executable: ${filePath}`);
331
+ }
332
+ }
333
+
334
+ async function isRunnable(filePath, platform) {
335
+ try {
336
+ const mode = platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK;
337
+ await fsp.access(filePath, mode);
338
+ return true;
339
+ } catch {
340
+ return false;
341
+ }
342
+ }
343
+
344
+ function spawnKin(binaryPath, args, options) {
345
+ const env = options.env || process.env;
346
+
347
+ return new Promise((resolve, reject) => {
348
+ const child = cp.spawn(binaryPath, args, {
349
+ cwd: options.cwd || process.cwd(),
350
+ env,
351
+ stdio: options.stdio || 'inherit'
352
+ });
353
+
354
+ const handlers = new Map();
355
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
356
+ const handler = () => {
357
+ if (!child.killed) {
358
+ child.kill(signal);
359
+ }
360
+ };
361
+ handlers.set(signal, handler);
362
+ process.on(signal, handler);
363
+ }
364
+
365
+ const cleanup = () => {
366
+ for (const [signal, handler] of handlers.entries()) {
367
+ process.off(signal, handler);
368
+ }
369
+ };
370
+
371
+ child.on('error', error => {
372
+ cleanup();
373
+ reject(error);
374
+ });
375
+
376
+ child.on('exit', (code, signal) => {
377
+ cleanup();
378
+ if (signal) {
379
+ resolve(1);
380
+ return;
381
+ }
382
+ resolve(code ?? 1);
383
+ });
384
+ });
385
+ }