@kinlab/kin-mcp 0.2.1 → 0.2.3

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 (3) hide show
  1. package/README.md +36 -4
  2. package/package.json +3 -2
  3. package/src/index.js +157 -24
package/README.md CHANGED
@@ -4,12 +4,27 @@
4
4
  command is still `kin-mcp`).
5
5
 
6
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:
7
+ SHA-256 checksum, extracts both the `kin` CLI and the `kin-daemon` it depends on
8
+ into a local cache, and runs:
8
9
 
9
10
  ```sh
10
11
  kin mcp start
11
12
  ```
12
13
 
14
+ On first run it:
15
+
16
+ - provisions `kin-daemon` next to `kin` and points the CLI at it with
17
+ `KIN_DAEMON_BIN`, so the MCP path never depends on a locally-built `kin`, a
18
+ stale daemon on `PATH`, or a pre-existing daemon;
19
+ - defaults `KIN_MCP_TOOL_PROFILE=agent-default`, so agents see the small curated
20
+ tool surface instead of the full internal one;
21
+ - starts (or reuses) the repo daemon automatically, and only then serves tools;
22
+ - requires an explicit `kin init` (no silent init) unless you opt in with
23
+ `KIN_MCP_AUTO_INIT=1`.
24
+
25
+ If a runnable Kin cannot be provisioned (unsupported target, release download
26
+ blocked), the wrapper exits with a precise guided fix instead of a stack trace.
27
+
13
28
  ## Usage
14
29
 
15
30
  Use it directly from an MCP client with `npx`:
@@ -48,7 +63,7 @@ Windows users should run Kin through WSL2 during the alpha.
48
63
 
49
64
  ## Cache
50
65
 
51
- The wrapper caches the downloaded `kin` binary under:
66
+ The wrapper caches the downloaded `kin` and `kin-daemon` binaries under:
52
67
 
53
68
  - macOS: `~/Library/Caches/kin-mcp`
54
69
  - Linux: `~/.cache/kin-mcp`
@@ -57,16 +72,33 @@ Set `KIN_MCP_CACHE_DIR` to override the cache location.
57
72
 
58
73
  ## Environment
59
74
 
60
- - `KIN_MCP_KIN_BINARY`: run a specific local `kin` binary instead of downloading one
75
+ - `KIN_MCP_KIN_BINARY`: run a specific local `kin` binary instead of downloading
76
+ one (you are then responsible for its `kin-daemon`)
61
77
  - `KIN_BINARY_PATH`: alias for `KIN_MCP_KIN_BINARY`
62
78
  - `KIN_MCP_CACHE_DIR`: override the cache directory
63
79
  - `KIN_MCP_AUTO_INIT=1`: allow the wrapper to run `kin init .` when `.kin/` is missing
80
+ - `KIN_MCP_TOOL_PROFILE`: override the default `agent-default` tool profile
64
81
  - `KIN_MCP_RELEASE_BASE_URL`: override the GitHub release download base URL
65
82
 
66
83
  ## Local Check
67
84
 
68
85
  ```sh
69
- npx -y @kinlab/kin-mcp --print-bin
86
+ npx -y @kinlab/kin-mcp --print-bin # provisioned kin path
87
+ npx -y @kinlab/kin-mcp --print-daemon-bin # provisioned kin-daemon path
70
88
  ```
71
89
 
72
90
  Then initialize a repository and let the MCP client launch `kin-mcp`.
91
+
92
+ ## First-run smoke proof
93
+
94
+ `test/smoke-first-run.mjs` exercises the whole first-run path against built Kin
95
+ binaries: it stages `kin` + `kin-daemon` into a throwaway cache (no pre-existing
96
+ daemon, no dev `PATH` state), runs the wrapper, and drives the MCP stdio protocol
97
+ through one safe semantic tool (`kin_graph_status`). Run it with:
98
+
99
+ ```sh
100
+ cargo build --release -p kin-cli -p kin-daemon
101
+ KIN_BIN=target/release/kin \
102
+ KIN_DAEMON_BIN=target/release/kin-daemon \
103
+ npm run smoke
104
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinlab/kin-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Start Kin's MCP server through an npm-friendly wrapper.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "test": "node --test",
17
- "lint": "node --check ./bin/kin-mcp.js && node --check ./src/index.js && node --check ./test/index.test.js"
17
+ "lint": "node --check ./bin/kin-mcp.js && node --check ./src/index.js && node --check ./test/index.test.js && node --check ./test/smoke-first-run.mjs",
18
+ "smoke": "node ./test/smoke-first-run.mjs"
18
19
  },
19
20
  "engines": {
20
21
  "node": ">=20"
package/src/index.js CHANGED
@@ -20,33 +20,40 @@ export function resolveReleaseTag(version = PACKAGE_VERSION) {
20
20
  return version.startsWith('v') ? version : `v${version}`;
21
21
  }
22
22
 
23
+ export const PRIMARY_BINARY_NAME = 'kin';
24
+ export const DAEMON_BINARY_NAME = 'kin-daemon';
25
+
23
26
  export function resolveReleaseAsset(platform = process.platform, arch = process.arch) {
24
27
  if (platform === 'darwin' && arch === 'arm64') {
25
28
  return {
26
29
  assetName: 'kin-macos-aarch64',
27
30
  archiveName: 'kin-macos-aarch64.tar.gz',
28
- binaryName: 'kin'
31
+ binaryName: PRIMARY_BINARY_NAME,
32
+ daemonBinaryName: DAEMON_BINARY_NAME
29
33
  };
30
34
  }
31
35
  if (platform === 'darwin' && arch === 'x64') {
32
36
  return {
33
37
  assetName: 'kin-macos-x86_64',
34
38
  archiveName: 'kin-macos-x86_64.tar.gz',
35
- binaryName: 'kin'
39
+ binaryName: PRIMARY_BINARY_NAME,
40
+ daemonBinaryName: DAEMON_BINARY_NAME
36
41
  };
37
42
  }
38
43
  if (platform === 'linux' && arch === 'x64') {
39
44
  return {
40
45
  assetName: 'kin-linux-x86_64',
41
46
  archiveName: 'kin-linux-x86_64.tar.gz',
42
- binaryName: 'kin'
47
+ binaryName: PRIMARY_BINARY_NAME,
48
+ daemonBinaryName: DAEMON_BINARY_NAME
43
49
  };
44
50
  }
45
51
  if (platform === 'linux' && arch === 'arm64') {
46
52
  return {
47
53
  assetName: 'kin-linux-aarch64',
48
54
  archiveName: 'kin-linux-aarch64.tar.gz',
49
- binaryName: 'kin'
55
+ binaryName: PRIMARY_BINARY_NAME,
56
+ daemonBinaryName: DAEMON_BINARY_NAME
50
57
  };
51
58
  }
52
59
 
@@ -115,11 +122,16 @@ export async function ensureKinBinary({
115
122
  cacheRoot
116
123
  });
117
124
 
118
- if (await isRunnable(binaryPath, platform)) {
125
+ const daemonPath = path.join(path.dirname(binaryPath), DAEMON_BINARY_NAME);
126
+ if (
127
+ (await isRunnable(binaryPath, platform)) &&
128
+ (await isRunnable(daemonPath, platform))
129
+ ) {
119
130
  return binaryPath;
120
131
  }
121
132
 
122
133
  await installKinBinary({ binaryPath, env, platform, arch, version });
134
+ await assertDaemonProvisioned(binaryPath, platform);
123
135
  return binaryPath;
124
136
  }
125
137
 
@@ -127,6 +139,7 @@ export async function runKinMcp(argv = [], options = {}) {
127
139
  const stdout = options.stdout || process.stdout;
128
140
  const stderr = options.stderr || process.stderr;
129
141
  const env = options.env || process.env;
142
+ const platform = options.platform || process.platform;
130
143
 
131
144
  if (argv.includes('--help') || argv.includes('-h')) {
132
145
  stdout.write(renderHelp());
@@ -138,9 +151,18 @@ export async function runKinMcp(argv = [], options = {}) {
138
151
  return 0;
139
152
  }
140
153
 
141
- if (argv.includes('--print-bin')) {
142
- const binaryPath = await ensureKinBinary(options);
143
- stdout.write(`${binaryPath}\n`);
154
+ if (argv.includes('--print-bin') || argv.includes('--print-daemon-bin')) {
155
+ let binaryPath;
156
+ try {
157
+ binaryPath = await ensureKinBinary(options);
158
+ } catch (error) {
159
+ stderr.write(`${guidedProvisioningFailure(error)}\n`);
160
+ return 1;
161
+ }
162
+ const target = argv.includes('--print-daemon-bin')
163
+ ? resolveDaemonBinaryPath(binaryPath)
164
+ : binaryPath;
165
+ stdout.write(`${target}\n`);
144
166
  return 0;
145
167
  }
146
168
 
@@ -151,7 +173,18 @@ export async function runKinMcp(argv = [], options = {}) {
151
173
  return 2;
152
174
  }
153
175
 
154
- const binaryPath = await ensureKinBinary(options);
176
+ let binaryPath;
177
+ try {
178
+ binaryPath = await ensureKinBinary(options);
179
+ } catch (error) {
180
+ stderr.write(`${guidedProvisioningFailure(error)}\n`);
181
+ return 1;
182
+ }
183
+
184
+ const spawnOptions = {
185
+ ...options,
186
+ env: childEnv(env, binaryPath, platform)
187
+ };
155
188
 
156
189
  const cwd = options.cwd || process.cwd();
157
190
  if (!await kinRepoExists(cwd)) {
@@ -162,14 +195,45 @@ export async function runKinMcp(argv = [], options = {}) {
162
195
  return 2;
163
196
  }
164
197
  stderr.write('No .kin/ found; KIN_MCP_AUTO_INIT=1, running kin init...\n');
165
- const initCode = await spawnKin(binaryPath, ['init', '.'], { ...options, cwd });
198
+ const initCode = await spawnKin(binaryPath, ['init', '.'], { ...spawnOptions, cwd });
166
199
  if (initCode !== 0) {
167
200
  stderr.write('kin init failed. Cannot start MCP server.\n');
168
201
  return initCode;
169
202
  }
170
203
  }
171
204
 
172
- return spawnKin(binaryPath, ['mcp', 'start'], options);
205
+ return spawnKin(binaryPath, ['mcp', 'start'], spawnOptions);
206
+ }
207
+
208
+ export function resolveDaemonBinaryPath(kinBinaryPath) {
209
+ return path.join(path.dirname(kinBinaryPath), DAEMON_BINARY_NAME);
210
+ }
211
+
212
+ export function childEnv(env, kinBinaryPath, platform = process.platform) {
213
+ const next = { ...env };
214
+
215
+ if (!next.KIN_MCP_TOOL_PROFILE) {
216
+ next.KIN_MCP_TOOL_PROFILE = 'agent-default';
217
+ }
218
+
219
+ const usesManagedBinary = !(env.KIN_MCP_KIN_BINARY || env.KIN_BINARY_PATH);
220
+ if (usesManagedBinary && !next.KIN_DAEMON_BIN) {
221
+ next.KIN_DAEMON_BIN = resolveDaemonBinaryPath(kinBinaryPath);
222
+ }
223
+
224
+ return next;
225
+ }
226
+
227
+ function guidedProvisioningFailure(error) {
228
+ const detail = error && error.message ? error.message : String(error);
229
+ return [
230
+ `kin-mcp could not provision a runnable Kin: ${detail}`,
231
+ 'Fixes:',
232
+ ' - Install Kin directly and run `kin setup` to configure your agent, then point',
233
+ ' this wrapper at it with KIN_MCP_KIN_BINARY=/path/to/kin.',
234
+ ' - Or retry on a supported target (macOS/Linux). On Windows use WSL2 for this alpha.',
235
+ ' - Or override the release source with KIN_MCP_RELEASE_BASE_URL if you mirror releases.'
236
+ ].join('\n');
173
237
  }
174
238
 
175
239
  function isTruthyEnv(value) {
@@ -182,25 +246,34 @@ function renderHelp() {
182
246
  Usage:
183
247
  kin-mcp
184
248
  kin-mcp --print-bin
249
+ kin-mcp --print-daemon-bin
185
250
  kin-mcp --version
186
251
 
187
- This wrapper downloads a matching Kin release binary on demand, caches it
188
- locally, and then runs:
252
+ This wrapper downloads a matching Kin release archive on demand, extracts both
253
+ the kin CLI and the kin-daemon it depends on into a local cache, and then runs:
189
254
 
190
255
  kin mcp start
191
256
 
257
+ It defaults KIN_MCP_TOOL_PROFILE=agent-default so first-run agents see the small
258
+ curated tool surface, and points the kin CLI at the cached kin-daemon via
259
+ KIN_DAEMON_BIN so it never depends on a stale daemon on PATH.
260
+
192
261
  Environment:
193
- KIN_MCP_KIN_BINARY Use a specific kin binary
262
+ KIN_MCP_KIN_BINARY Use a specific kin binary (you manage its kin-daemon)
194
263
  KIN_BINARY_PATH Alias for KIN_MCP_KIN_BINARY
195
264
  KIN_MCP_CACHE_DIR Override the cache directory
196
265
  KIN_MCP_AUTO_INIT Set to 1 to allow wrapper-initiated kin init
266
+ KIN_MCP_TOOL_PROFILE Override the default agent-default tool profile
197
267
  KIN_MCP_RELEASE_BASE_URL
198
268
  Override the release download base URL
199
269
  `;
200
270
  }
201
271
 
202
272
  async function installKinBinary({ binaryPath, env, platform, arch, version }) {
203
- const { assetName, archiveName, binaryName } = resolveReleaseAsset(platform, arch);
273
+ const { assetName, archiveName, binaryName, daemonBinaryName } = resolveReleaseAsset(
274
+ platform,
275
+ arch
276
+ );
204
277
  const tag = resolveReleaseTag(version);
205
278
  const baseUrl = (env.KIN_MCP_RELEASE_BASE_URL || DEFAULT_RELEASE_BASE_URL).replace(
206
279
  /\/$/,
@@ -227,6 +300,7 @@ async function installKinBinary({ binaryPath, env, platform, arch, version }) {
227
300
  archiveName,
228
301
  assetName,
229
302
  binaryName,
303
+ daemonBinaryName,
230
304
  binaryPath,
231
305
  platform
232
306
  });
@@ -237,32 +311,82 @@ async function installFromArchive({
237
311
  archiveName,
238
312
  assetName,
239
313
  binaryName,
314
+ daemonBinaryName,
240
315
  binaryPath,
241
316
  platform
242
317
  }) {
243
318
  const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'kin-mcp-install-'));
244
319
  const archivePath = path.join(tmpRoot, archiveName);
245
- const tmpPath = `${binaryPath}.download`;
320
+ const installDir = path.dirname(binaryPath);
321
+ const daemonPath = path.join(installDir, daemonBinaryName);
322
+ const staged = [];
246
323
 
247
324
  try {
248
325
  await fsp.writeFile(archivePath, archiveBytes);
249
326
  await execFile('tar', ['-xzf', archivePath, '-C', tmpRoot]);
250
327
 
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);
328
+ const kinStaged = await stageBinary({
329
+ tmpRoot,
330
+ assetName,
331
+ sourceName: binaryName,
332
+ destination: binaryPath,
333
+ platform,
334
+ required: true
335
+ });
336
+ staged.push(kinStaged);
337
+
338
+ const daemonStaged = await stageBinary({
339
+ tmpRoot,
340
+ assetName,
341
+ sourceName: daemonBinaryName,
342
+ destination: daemonPath,
343
+ platform,
344
+ required: true,
345
+ missingHint:
346
+ 'the published release archive is missing kin-daemon; the daemon is required for MCP'
347
+ });
348
+ staged.push(daemonStaged);
349
+
350
+ for (const item of staged) {
351
+ await fsp.rename(item.tmpPath, item.destination);
256
352
  }
257
- await fsp.rename(tmpPath, binaryPath);
258
353
  } catch (error) {
259
- await fsp.unlink(tmpPath).catch(() => {});
260
- throw new Error(`failed to install ${binaryName} from ${archiveName}: ${error.message}`);
354
+ await Promise.all(staged.map(item => fsp.unlink(item.tmpPath).catch(() => {})));
355
+ throw new Error(
356
+ `failed to install ${binaryName} from ${archiveName}: ${error.message}`
357
+ );
261
358
  } finally {
262
359
  await fsp.rm(tmpRoot, { recursive: true, force: true });
263
360
  }
264
361
  }
265
362
 
363
+ async function stageBinary({
364
+ tmpRoot,
365
+ assetName,
366
+ sourceName,
367
+ destination,
368
+ platform,
369
+ required,
370
+ missingHint
371
+ }) {
372
+ const extracted = path.join(tmpRoot, assetName, sourceName);
373
+ try {
374
+ await fsp.access(extracted, fs.constants.R_OK);
375
+ } catch {
376
+ if (required) {
377
+ throw new Error(missingHint || `archive is missing ${sourceName}`);
378
+ }
379
+ return null;
380
+ }
381
+
382
+ const tmpPath = `${destination}.download`;
383
+ await fsp.copyFile(extracted, tmpPath);
384
+ if (platform !== 'win32') {
385
+ await fsp.chmod(tmpPath, 0o755);
386
+ }
387
+ return { destination, tmpPath };
388
+ }
389
+
266
390
  function execFile(file, args, options = {}) {
267
391
  return new Promise((resolve, reject) => {
268
392
  cp.execFile(file, args, options, (error, stdout, stderr) => {
@@ -331,6 +455,15 @@ async function assertRunnable(filePath, platform) {
331
455
  }
332
456
  }
333
457
 
458
+ async function assertDaemonProvisioned(kinBinaryPath, platform) {
459
+ const daemonPath = resolveDaemonBinaryPath(kinBinaryPath);
460
+ if (!(await isRunnable(daemonPath, platform))) {
461
+ throw new Error(
462
+ `kin-daemon was not provisioned next to ${kinBinaryPath}; the MCP server cannot start without it`
463
+ );
464
+ }
465
+ }
466
+
334
467
  async function isRunnable(filePath, platform) {
335
468
  try {
336
469
  const mode = platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK;