@kinlab/kin-mcp 0.2.0 → 0.2.2
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 +36 -4
- package/package.json +3 -2
- 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`
|
|
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`
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.2.2",
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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', '.'], { ...
|
|
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'],
|
|
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
|
|
188
|
-
|
|
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(
|
|
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
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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(
|
|
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;
|