@aikdna/kdna-mcp-server 0.1.0 → 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 +3 -0
- package/bin/kdna-mcp.mjs +55 -0
- package/package.json +2 -2
- package/test/mcp-v1.test.mjs +80 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ The `kdna-loader` skill teaches an agent the full KDNA protocol (7-part routing,
|
|
|
18
18
|
|------|---------|-------|--------|
|
|
19
19
|
| `kdna.inspect` | Inspect a v1 `.kdna` asset or legacy asset | File path | Structured metadata |
|
|
20
20
|
| `kdna.verify` | Verify asset integrity state | File path | Pass/fail with reasons |
|
|
21
|
+
| `kdna.plan-load` | Return Core LoadPlan before loading | File path, optional password or entitlement state | LoadPlan JSON |
|
|
21
22
|
| `kdna.load` | Load and render a `.kdna` profile for agent context | File path, optional profile | Prompt-mode text or raw JSON |
|
|
22
23
|
| `kdna.available-local` | List local v1 `.kdna` assets without registry dependency | Root directory | Local v1 asset inventory |
|
|
23
24
|
| `kdna.match` | Rank candidate assets for a task string | Task description | Scored list with fit signals |
|
|
@@ -92,6 +93,8 @@ node bin/kdna-mcp.mjs
|
|
|
92
93
|
```
|
|
93
94
|
# Client calls kdna.available-local { root: "./dist" }
|
|
94
95
|
# Client calls kdna.verify { assetPath: "./dist/writing.kdna" }
|
|
96
|
+
# Client calls kdna.plan-load { assetPath: "./dist/writing.kdna" }
|
|
97
|
+
# Or kdna.plan-load { assetPath: "./dist/writing.kdna", entitlementStatus: "active" }
|
|
95
98
|
# Client calls kdna.load { assetPath: "./dist/writing.kdna", profile: "compact" }
|
|
96
99
|
# Agent injects prompt text into system context
|
|
97
100
|
```
|
package/bin/kdna-mcp.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import readline from 'node:readline';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
6
7
|
import { createRequire } from 'node:module';
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
@@ -10,6 +11,7 @@ const {
|
|
|
10
11
|
inspectKDNA,
|
|
11
12
|
loadKDNA,
|
|
12
13
|
matchDomain,
|
|
14
|
+
planLoad,
|
|
13
15
|
renderForAgent,
|
|
14
16
|
verifyAsset,
|
|
15
17
|
} = require('@aikdna/kdna-core');
|
|
@@ -58,6 +60,19 @@ const tools = [
|
|
|
58
60
|
},
|
|
59
61
|
},
|
|
60
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: 'kdna.plan-load',
|
|
65
|
+
description: 'Return the Core LoadPlan for a .kdna asset before loading.',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
required: ['assetPath'],
|
|
69
|
+
properties: {
|
|
70
|
+
assetPath: { type: 'string' },
|
|
71
|
+
hasPassword: { type: 'boolean' },
|
|
72
|
+
entitlementStatus: { type: 'string', enum: ['active', 'expired', 'revoked', 'offline_grace'] },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
61
76
|
{
|
|
62
77
|
name: 'kdna.available-local',
|
|
63
78
|
description: 'List local v1 .kdna assets or v1 source directories without using a registry.',
|
|
@@ -191,6 +206,43 @@ function listRegistry(registryFile) {
|
|
|
191
206
|
}));
|
|
192
207
|
}
|
|
193
208
|
|
|
209
|
+
function runCliPlanLoad(args = {}) {
|
|
210
|
+
const cliArgs = ['plan-load', args.assetPath, '--json'];
|
|
211
|
+
if (args.hasPassword) cliArgs.push('--has-password');
|
|
212
|
+
if (args.entitlementStatus) cliArgs.push('--entitlement-status', args.entitlementStatus);
|
|
213
|
+
|
|
214
|
+
const result = spawnSync('kdna', cliArgs, {
|
|
215
|
+
encoding: 'utf8',
|
|
216
|
+
timeout: 30_000,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (result.error) {
|
|
220
|
+
throw new Error(`Core planLoad is unavailable and kdna CLI failed: ${result.error.message}`);
|
|
221
|
+
}
|
|
222
|
+
if (result.status !== 0) {
|
|
223
|
+
throw new Error(`Core planLoad is unavailable and kdna CLI exited ${result.status}: ${result.stderr || result.stdout}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(result.stdout);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw new Error(`Core planLoad is unavailable and kdna CLI returned non-JSON output: ${error.message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function planLoadThroughCoreOrCli(args = {}) {
|
|
234
|
+
if (!args.assetPath) throw new Error('assetPath is required');
|
|
235
|
+
|
|
236
|
+
if (typeof planLoad === 'function' && process.env.KDNA_MCP_FORCE_CLI_PLAN_LOAD !== '1') {
|
|
237
|
+
return planLoad(args.assetPath, {
|
|
238
|
+
hasPassword: Boolean(args.hasPassword),
|
|
239
|
+
entitlement: args.entitlementStatus ? { status: args.entitlementStatus } : undefined,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return runCliPlanLoad(args);
|
|
244
|
+
}
|
|
245
|
+
|
|
194
246
|
async function callTool(name, args = {}) {
|
|
195
247
|
if (name === 'kdna.inspect') {
|
|
196
248
|
if (isV1Asset(args.assetPath)) return textResult(inspectV1(args.assetPath));
|
|
@@ -217,6 +269,9 @@ async function callTool(name, args = {}) {
|
|
|
217
269
|
: await renderForAgent(args.assetPath, { profile: args.profile || 'compact', input: args.input || '' });
|
|
218
270
|
return textResult({ ...loaded, context });
|
|
219
271
|
}
|
|
272
|
+
if (name === 'kdna.plan-load') {
|
|
273
|
+
return textResult(planLoadThroughCoreOrCli(args));
|
|
274
|
+
}
|
|
220
275
|
if (name === 'kdna.match') {
|
|
221
276
|
return textResult(await matchDomain(args.input || '', args.assetPaths || []));
|
|
222
277
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for loading, inspecting, verifying, and matching KDNA .kdna assets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@aikdna/kdna-core": "^0.
|
|
14
|
+
"@aikdna/kdna-core": "^0.12.0",
|
|
15
15
|
"ajv": "^8.20.0",
|
|
16
16
|
"ajv-formats": "^3.0.1"
|
|
17
17
|
},
|
package/test/mcp-v1.test.mjs
CHANGED
|
@@ -11,7 +11,7 @@ const { buildChecksumsV1 } = require('@aikdna/kdna-core/v1');
|
|
|
11
11
|
|
|
12
12
|
const server = path.join(process.cwd(), 'bin', 'kdna-mcp.mjs');
|
|
13
13
|
|
|
14
|
-
function callTool(name, args) {
|
|
14
|
+
function callTool(name, args, options = {}) {
|
|
15
15
|
const request = {
|
|
16
16
|
jsonrpc: '2.0',
|
|
17
17
|
id: 1,
|
|
@@ -21,6 +21,7 @@ function callTool(name, args) {
|
|
|
21
21
|
const r = spawnSync(process.execPath, [server], {
|
|
22
22
|
input: `${JSON.stringify(request)}\n`,
|
|
23
23
|
encoding: 'utf8',
|
|
24
|
+
env: options.env || process.env,
|
|
24
25
|
});
|
|
25
26
|
assert.equal(r.status, 0, r.stderr);
|
|
26
27
|
const response = JSON.parse(r.stdout.trim());
|
|
@@ -28,6 +29,18 @@ function callTool(name, args) {
|
|
|
28
29
|
return JSON.parse(response.result.content[0].text);
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function listTools() {
|
|
33
|
+
const request = { jsonrpc: '2.0', id: 1, method: 'tools/list' };
|
|
34
|
+
const r = spawnSync(process.execPath, [server], {
|
|
35
|
+
input: `${JSON.stringify(request)}\n`,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
});
|
|
38
|
+
assert.equal(r.status, 0, r.stderr);
|
|
39
|
+
const response = JSON.parse(r.stdout.trim());
|
|
40
|
+
assert.ok(!response.error, response.error && response.error.message);
|
|
41
|
+
return response.result.tools;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
function makeV1Source(root) {
|
|
32
45
|
const dir = path.join(root, 'writing');
|
|
33
46
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -91,3 +104,69 @@ test('available-local discovers v1 source dirs and load returns prompt context',
|
|
|
91
104
|
fs.rmSync(root, { recursive: true, force: true });
|
|
92
105
|
}
|
|
93
106
|
});
|
|
107
|
+
|
|
108
|
+
test('plan-load uses the Core API when available', () => {
|
|
109
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-mcp-plan-core-'));
|
|
110
|
+
try {
|
|
111
|
+
const assetPath = makeV1Source(root);
|
|
112
|
+
const tools = listTools();
|
|
113
|
+
assert.ok(tools.some((tool) => tool.name === 'kdna.plan-load'));
|
|
114
|
+
|
|
115
|
+
const plan = callTool('kdna.plan-load', { assetPath });
|
|
116
|
+
|
|
117
|
+
assert.equal(plan.state, 'ready');
|
|
118
|
+
assert.equal(plan.required_action, 'load');
|
|
119
|
+
assert.equal(plan.can_load_now, true);
|
|
120
|
+
assert.equal(plan.asset.asset_id, 'kdna:test:writing');
|
|
121
|
+
} finally {
|
|
122
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('plan-load can fall back to the official CLI when Core API is unavailable', () => {
|
|
127
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-mcp-plan-'));
|
|
128
|
+
try {
|
|
129
|
+
const assetPath = makeV1Source(root);
|
|
130
|
+
const binDir = path.join(root, 'bin');
|
|
131
|
+
const argvFile = path.join(root, 'argv.json');
|
|
132
|
+
fs.mkdirSync(binDir);
|
|
133
|
+
const kdnaBin = path.join(binDir, 'kdna');
|
|
134
|
+
fs.writeFileSync(kdnaBin, `#!/usr/bin/env node
|
|
135
|
+
const fs = require('node:fs');
|
|
136
|
+
fs.writeFileSync(${JSON.stringify(argvFile)}, JSON.stringify(process.argv.slice(2)));
|
|
137
|
+
console.log(JSON.stringify({
|
|
138
|
+
state: "ready",
|
|
139
|
+
required_action: "none",
|
|
140
|
+
can_load_now: true,
|
|
141
|
+
issues: [{ code: "KDNA_OK" }],
|
|
142
|
+
asset: { source: process.argv[3] }
|
|
143
|
+
}));
|
|
144
|
+
`);
|
|
145
|
+
fs.chmodSync(kdnaBin, 0o755);
|
|
146
|
+
|
|
147
|
+
const tools = listTools();
|
|
148
|
+
assert.ok(tools.some((tool) => tool.name === 'kdna.plan-load'));
|
|
149
|
+
|
|
150
|
+
const plan = callTool('kdna.plan-load', { assetPath, hasPassword: true, entitlementStatus: 'active' }, {
|
|
151
|
+
env: {
|
|
152
|
+
...process.env,
|
|
153
|
+
KDNA_MCP_FORCE_CLI_PLAN_LOAD: '1',
|
|
154
|
+
PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
assert.equal(plan.state, 'ready');
|
|
159
|
+
assert.equal(plan.required_action, 'none');
|
|
160
|
+
assert.equal(plan.can_load_now, true);
|
|
161
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(argvFile, 'utf8')), [
|
|
162
|
+
'plan-load',
|
|
163
|
+
assetPath,
|
|
164
|
+
'--json',
|
|
165
|
+
'--has-password',
|
|
166
|
+
'--entitlement-status',
|
|
167
|
+
'active',
|
|
168
|
+
]);
|
|
169
|
+
} finally {
|
|
170
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
});
|