@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 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.1.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.11.1",
14
+ "@aikdna/kdna-core": "^0.12.0",
15
15
  "ajv": "^8.20.0",
16
16
  "ajv-formats": "^3.0.1"
17
17
  },
@@ -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
+ });