@aikdna/kdna-mcp-server 0.4.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/bin/kdna-mcp.mjs +172 -34
  3. package/package.json +10 -4
package/README.md CHANGED
@@ -22,13 +22,18 @@ server provides a lower-level bridge for structured tool calls.
22
22
  |------|---------|-------|--------|
23
23
  | `kdna.inspect` | Inspect a `.kdna` asset | File path | Structured metadata |
24
24
  | `kdna.verify` | Verify asset integrity state | File path | Pass/fail with reasons |
25
- | `kdna.plan-load` | Return Core LoadPlan before loading | File path, optional password or entitlement state | LoadPlan JSON |
25
+ | `kdna.plan-load` | Return Core LoadPlan before loading | File path, optional password-presence or entitlement state | LoadPlan JSON |
26
26
  | `kdna.load` | Load an authorized `.kdna` profile | File path, optional profile, password, or entitlement state | Runtime Capsule |
27
27
  | `kdna.available-local` | List local `.kdna` assets | Root directory | Local asset inventory |
28
28
  | `kdna.match` | Rank candidate assets for a task string | Task description | Scored list with fit signals |
29
29
 
30
30
  ## Install & Run
31
31
 
32
+ The commands below currently resolve the published MCP 0.4.1 package. They do
33
+ not install or prove the MCP 0.4.2 / Core 0.20.0 source checkpoint described in
34
+ this repository's current changelog; those candidates must remain a coordinated
35
+ package set and must not be mixed with the published wave.
36
+
32
37
  ```bash
33
38
  npm install -g @aikdna/kdna-mcp-server
34
39
  kdna-mcp
@@ -106,6 +111,14 @@ node bin/kdna-mcp.mjs
106
111
  # Agent consumes only the returned Runtime Capsule context
107
112
  ```
108
113
 
114
+ In the 0.4.2 source checkpoint, the returned Capsule uses
115
+ `type: "kdna.runtime-capsule"` and
116
+ `contract_version: "0.1.0"`. The KDNA Agent Host protocol has the independent
117
+ coordinate `protocol_version: "0.1.0"`; it is not the MCP transport's
118
+ `protocolVersion`. This bridge does not execute an Agent Host and therefore
119
+ does not create or claim a Host receipt or Judgment Trace. A consuming Host
120
+ must use Core's correlated runtime-contract APIs for that boundary.
121
+
109
122
  ## Relationship to kdna-loader skill
110
123
 
111
124
  The MCP server and kdna-loader skill are complementary:
package/bin/kdna-mcp.mjs CHANGED
@@ -7,6 +7,10 @@ import { spawnSync } from 'node:child_process';
7
7
  import { createRequire } from 'node:module';
8
8
 
9
9
  const require = createRequire(import.meta.url);
10
+ const Ajv = require('ajv');
11
+ const Ajv2020 = require('ajv/dist/2020');
12
+ const addFormats = require('ajv-formats');
13
+ const loadPlanSchema = require('@aikdna/kdna-core/schema/load-plan.schema.json');
10
14
  const packageInfo = require('../package.json');
11
15
  const {
12
16
  detectContainerFormat,
@@ -23,6 +27,7 @@ const tools = [
23
27
  description: 'Inspect a .kdna asset without extracting it.',
24
28
  inputSchema: {
25
29
  type: 'object',
30
+ additionalProperties: false,
26
31
  required: ['assetPath'],
27
32
  properties: { assetPath: { type: 'string' }, verify: { type: 'boolean' } },
28
33
  },
@@ -32,6 +37,7 @@ const tools = [
32
37
  description: 'Verify a .kdna asset integrity state.',
33
38
  inputSchema: {
34
39
  type: 'object',
40
+ additionalProperties: false,
35
41
  required: ['assetPath'],
36
42
  properties: {
37
43
  assetPath: { type: 'string' },
@@ -46,6 +52,7 @@ const tools = [
46
52
  description: 'Load a .kdna profile and return agent context.',
47
53
  inputSchema: {
48
54
  type: 'object',
55
+ additionalProperties: false,
49
56
  required: ['assetPath'],
50
57
  properties: {
51
58
  assetPath: { type: 'string' },
@@ -61,6 +68,7 @@ const tools = [
61
68
  description: 'Return the Core LoadPlan for a .kdna asset before loading.',
62
69
  inputSchema: {
63
70
  type: 'object',
71
+ additionalProperties: false,
64
72
  required: ['assetPath'],
65
73
  properties: {
66
74
  assetPath: { type: 'string' },
@@ -74,9 +82,10 @@ const tools = [
74
82
  description: 'List local .kdna files without using a registry.',
75
83
  inputSchema: {
76
84
  type: 'object',
85
+ additionalProperties: false,
77
86
  properties: {
78
87
  root: { type: 'string' },
79
- maxDepth: { type: 'number' },
88
+ maxDepth: { type: 'integer', minimum: 0 },
80
89
  },
81
90
  },
82
91
  },
@@ -85,6 +94,7 @@ const tools = [
85
94
  description: 'Rank .kdna assets for a task string.',
86
95
  inputSchema: {
87
96
  type: 'object',
97
+ additionalProperties: false,
88
98
  required: ['input', 'assetPaths'],
89
99
  properties: {
90
100
  input: { type: 'string' },
@@ -94,11 +104,52 @@ const tools = [
94
104
  },
95
105
  ];
96
106
 
97
- function send(id, result, error) {
98
- const msg = error
99
- ? { jsonrpc: '2.0', id, error: { code: -32000, message: error.message || String(error) } }
100
- : { jsonrpc: '2.0', id, result };
101
- process.stdout.write(`${JSON.stringify(msg)}\n`);
107
+ const ajv = new Ajv({ allErrors: true, strict: false });
108
+ const toolValidators = new Map(tools.map((tool) => [tool.name, ajv.compile(tool.inputSchema)]));
109
+ const contractAjv = new Ajv2020({ allErrors: true, strict: false });
110
+ addFormats(contractAjv);
111
+ const validateLoadPlan = contractAjv.compile(loadPlanSchema);
112
+
113
+ class JsonRpcError extends Error {
114
+ constructor(code, message) {
115
+ super(message);
116
+ this.code = code;
117
+ }
118
+ }
119
+
120
+ function validatedJsonRpcMessage(message) {
121
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
122
+ throw new JsonRpcError(-32600, 'Invalid Request');
123
+ }
124
+ if (message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
125
+ throw new JsonRpcError(-32600, 'Invalid Request');
126
+ }
127
+
128
+ const hasId = Object.prototype.hasOwnProperty.call(message, 'id');
129
+ if (hasId) {
130
+ const validId =
131
+ typeof message.id === 'string' ||
132
+ (typeof message.id === 'number' && Number.isFinite(message.id));
133
+ if (!validId) throw new JsonRpcError(-32600, 'Invalid Request');
134
+ }
135
+
136
+ return {
137
+ id: message.id,
138
+ method: message.method,
139
+ params: message.params === undefined ? {} : message.params,
140
+ paramsValid:
141
+ message.params === undefined ||
142
+ (message.params !== null && typeof message.params === 'object' && !Array.isArray(message.params)),
143
+ isNotification: !hasId,
144
+ };
145
+ }
146
+
147
+ function sendResult(id, result) {
148
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
149
+ }
150
+
151
+ function sendError(id, code, message) {
152
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } })}\n`);
102
153
  }
103
154
 
104
155
  function textResult(value) {
@@ -107,6 +158,26 @@ function textResult(value) {
107
158
  };
108
159
  }
109
160
 
161
+ function toolErrorResult(error) {
162
+ return {
163
+ content: [{ type: 'text', text: error?.message || String(error) }],
164
+ isError: true,
165
+ };
166
+ }
167
+
168
+ function validatedToolCall(params) {
169
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
170
+ throw new JsonRpcError(-32602, 'Invalid params');
171
+ }
172
+ if (typeof params.name !== 'string' || !toolValidators.has(params.name)) {
173
+ throw new JsonRpcError(-32602, 'Invalid params');
174
+ }
175
+ const args = params.arguments === undefined ? {} : params.arguments;
176
+ const validateInput = toolValidators.get(params.name);
177
+ if (!validateInput(args)) throw new JsonRpcError(-32602, 'Invalid params');
178
+ return { name: params.name, args };
179
+ }
180
+
110
181
  function isKdnaAsset(assetPath) {
111
182
  if (!assetPath) return false;
112
183
  try {
@@ -139,20 +210,35 @@ function findLocalAssets(root = defaultAssetRoot(), maxDepth = 3) {
139
210
  const full = path.join(dir, entry.name);
140
211
  if (entry.isDirectory()) {
141
212
  visit(full, depth + 1);
142
- } else if (entry.isFile() && entry.name.endsWith('.kdna') && detectContainerFormat(full) === 'kdna') {
143
- const inspection = inspect(full);
144
- const validation = validate(full);
145
- found.push({
146
- path: full,
147
- kind: 'kdna_asset',
148
- asset_id: inspection.asset_id,
149
- title: inspection.title,
150
- version: inspection.version,
151
- judgment_version: inspection.judgment_version,
152
- checksums_present: Boolean(inspection.checksums_present),
153
- loadable: Boolean(validation.overall_valid),
154
- problems: validation.overall_valid ? [] : validation.problems || [],
155
- });
213
+ } else if (entry.isFile() && entry.name.endsWith('.kdna')) {
214
+ try {
215
+ if (detectContainerFormat(full) !== 'kdna') continue;
216
+ const inspection = inspect(full);
217
+ const validation = validate(full);
218
+ found.push({
219
+ path: full,
220
+ kind: 'kdna_asset',
221
+ asset_id: inspection.asset_id,
222
+ title: inspection.title,
223
+ version: inspection.version,
224
+ judgment_version: inspection.judgment_version,
225
+ checksums_present: Boolean(inspection.checksums_present),
226
+ loadable: Boolean(validation.overall_valid),
227
+ problems: validation.overall_valid ? [] : validation.problems || [],
228
+ });
229
+ } catch {
230
+ found.push({
231
+ path: full,
232
+ kind: 'kdna_asset',
233
+ asset_id: null,
234
+ title: null,
235
+ version: null,
236
+ judgment_version: null,
237
+ checksums_present: false,
238
+ loadable: false,
239
+ problems: ['KDNA_ASSET_INVALID'],
240
+ });
241
+ }
156
242
  }
157
243
  }
158
244
  }
@@ -174,15 +260,31 @@ function runCliPlanLoad(args = {}) {
174
260
  if (result.error) {
175
261
  throw new Error(`Core planLoad is unavailable and kdna CLI failed: ${result.error.message}`);
176
262
  }
263
+ if (result.status !== 0 && result.status !== 3) {
264
+ throw new Error(`Core planLoad is unavailable and kdna CLI exited ${result.status}`);
265
+ }
266
+ let plan;
177
267
  try {
178
- if (result.stdout && result.stdout.trim()) return JSON.parse(result.stdout);
268
+ if (result.stdout && result.stdout.trim()) plan = JSON.parse(result.stdout);
179
269
  } catch (error) {
180
270
  throw new Error(`Core planLoad is unavailable and kdna CLI returned non-JSON output: ${error.message}`);
181
271
  }
182
- if (result.status !== 0) {
183
- throw new Error(`Core planLoad is unavailable and kdna CLI exited ${result.status}: ${result.stderr || result.stdout}`);
272
+ if (!validateLoadPlan(plan)) {
273
+ throw new Error('Core planLoad is unavailable and kdna CLI returned an invalid LoadPlan');
184
274
  }
185
- throw new Error('Core planLoad is unavailable and kdna CLI returned empty output');
275
+ if (
276
+ plan.source.kind !== 'file' ||
277
+ path.resolve(plan.source.path) !== path.resolve(args.assetPath)
278
+ ) {
279
+ throw new Error('Core planLoad is unavailable and kdna CLI returned an uncorrelated LoadPlan');
280
+ }
281
+ if (result.status === 0 && plan.can_load_now !== true) {
282
+ throw new Error('Core planLoad is unavailable and kdna CLI returned a non-loadable success');
283
+ }
284
+ if (result.status === 3 && plan.can_load_now !== false) {
285
+ throw new Error('Core planLoad is unavailable and kdna CLI returned a loadable denial');
286
+ }
287
+ return plan;
186
288
  }
187
289
 
188
290
  function planLoadThroughCoreOrCli(args = {}) {
@@ -228,15 +330,14 @@ async function callTool(name, args = {}) {
228
330
  return textResult(await matchDomain(args.input || '', args.assetPaths || []));
229
331
  }
230
332
  if (name === 'kdna.available-local') {
231
- return textResult(findLocalAssets(args.root, args.maxDepth || 3));
333
+ return textResult(findLocalAssets(args.root, args.maxDepth ?? 3));
232
334
  }
233
335
  throw new Error(`Unknown tool: ${name}`);
234
336
  }
235
337
 
236
- async function handle(message) {
237
- const { id, method, params = {} } = message;
338
+ async function handle({ id, method, params }) {
238
339
  if (method === 'initialize') {
239
- send(id, {
340
+ sendResult(id, {
240
341
  protocolVersion: '2024-11-05',
241
342
  capabilities: { tools: {} },
242
343
  serverInfo: { name: '@aikdna/kdna-mcp-server', version: packageInfo.version },
@@ -244,14 +345,28 @@ async function handle(message) {
244
345
  return;
245
346
  }
246
347
  if (method === 'tools/list') {
247
- send(id, { tools });
348
+ sendResult(id, { tools });
248
349
  return;
249
350
  }
250
351
  if (method === 'tools/call') {
251
- send(id, await callTool(params.name, params.arguments || {}));
352
+ let call;
353
+ try {
354
+ call = validatedToolCall(params);
355
+ } catch (error) {
356
+ if (error instanceof JsonRpcError) {
357
+ sendError(id, error.code, error.message);
358
+ return;
359
+ }
360
+ throw error;
361
+ }
362
+ try {
363
+ sendResult(id, await callTool(call.name, call.args));
364
+ } catch (error) {
365
+ sendResult(id, toolErrorResult(error));
366
+ }
252
367
  return;
253
368
  }
254
- if (id !== undefined) send(id, {});
369
+ if (id !== undefined) sendError(id, -32601, 'Method not found');
255
370
  }
256
371
 
257
372
  const rl = readline.createInterface({ input: process.stdin });
@@ -260,8 +375,31 @@ rl.on('line', async (line) => {
260
375
  let message;
261
376
  try {
262
377
  message = JSON.parse(line);
263
- await handle(message);
264
- } catch (e) {
265
- send(message?.id ?? null, null, e);
378
+ } catch {
379
+ sendError(null, -32700, 'Parse error');
380
+ return;
381
+ }
382
+
383
+ let request;
384
+ try {
385
+ request = validatedJsonRpcMessage(message);
386
+ } catch (error) {
387
+ if (error instanceof JsonRpcError) {
388
+ sendError(null, error.code, error.message);
389
+ return;
390
+ }
391
+ sendError(null, -32603, 'Internal error');
392
+ return;
393
+ }
394
+
395
+ if (request.isNotification) return;
396
+ if (!request.paramsValid) {
397
+ sendError(request.id, -32602, 'Invalid params');
398
+ return;
399
+ }
400
+ try {
401
+ await handle(request);
402
+ } catch {
403
+ sendError(request.id, -32603, 'Internal error');
266
404
  }
267
405
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "MCP server for loading, inspecting, verifying, and matching KDNA .kdna assets.",
5
5
  "type": "module",
6
6
  "homepage": "https://aikdna.com",
@@ -29,12 +29,18 @@
29
29
  "NOTICE"
30
30
  ],
31
31
  "scripts": {
32
- "test": "node --test test/*.test.mjs",
33
- "prepublishOnly": "npm test && node ../scripts/release-check.js"
32
+ "test": "node scripts/verify-core-candidate.mjs && node ../scripts/check-current-names.mjs && node --test test/*.test.mjs",
33
+ "check:core-candidate": "node scripts/verify-core-candidate.mjs",
34
+ "check:current-names": "node ../scripts/check-current-names.mjs",
35
+ "naming:check": "node ../scripts/check-current-names.mjs",
36
+ "release:check": "node ../scripts/release-check.js",
37
+ "release:dependency-guard": "node ../scripts/release-dependency-guard.js",
38
+ "release:preflight": "node ../scripts/release-preflight.js",
39
+ "prepublishOnly": "npm test && npm run release:check && npm run release:dependency-guard"
34
40
  },
35
41
  "license": "Apache-2.0",
36
42
  "dependencies": {
37
- "@aikdna/kdna-core": "0.16.0",
43
+ "@aikdna/kdna-core": "0.20.0",
38
44
  "ajv": "^8.20.0",
39
45
  "ajv-formats": "^3.0.1"
40
46
  },