@aikdna/kdna-mcp-server 0.1.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 +105 -0
- package/bin/kdna-mcp.mjs +261 -0
- package/package.json +21 -0
- package/test/mcp-v1.test.mjs +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# KDNA MCP Server
|
|
2
|
+
|
|
3
|
+
`@aikdna/kdna-mcp-server` exposes KDNA as MCP tools so agent runtimes can use
|
|
4
|
+
`.kdna` assets without learning the container internals. Stdio-only, minimal footprint.
|
|
5
|
+
|
|
6
|
+
## Why MCP instead of the kdna-loader skill?
|
|
7
|
+
|
|
8
|
+
The `kdna-loader` skill teaches an agent the full KDNA protocol (7-part routing, silent loading, boundary respect). The MCP server provides a lower-level bridge — use it when you need programmatic access to `.kdna` assets through structured tool calls rather than letting the agent drive the CLI.
|
|
9
|
+
|
|
10
|
+
| Approach | Best for |
|
|
11
|
+
|----------|----------|
|
|
12
|
+
| `kdna-loader` skill | Full KDNA protocol: routing, fit evaluation, silent application |
|
|
13
|
+
| MCP server | Programmatic inspect/verify/load/match from any MCP-compatible runtime |
|
|
14
|
+
|
|
15
|
+
## Tools
|
|
16
|
+
|
|
17
|
+
| Tool | Purpose | Input | Output |
|
|
18
|
+
|------|---------|-------|--------|
|
|
19
|
+
| `kdna.inspect` | Inspect a v1 `.kdna` asset or legacy asset | File path | Structured metadata |
|
|
20
|
+
| `kdna.verify` | Verify asset integrity state | File path | Pass/fail with reasons |
|
|
21
|
+
| `kdna.load` | Load and render a `.kdna` profile for agent context | File path, optional profile | Prompt-mode text or raw JSON |
|
|
22
|
+
| `kdna.available-local` | List local v1 `.kdna` assets without registry dependency | Root directory | Local v1 asset inventory |
|
|
23
|
+
| `kdna.match` | Rank candidate assets for a task string | Task description | Scored list with fit signals |
|
|
24
|
+
| `kdna.available` | Legacy registry compatibility only | `domains.json` path | Legacy domain list |
|
|
25
|
+
|
|
26
|
+
## Install & Run
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @aikdna/kdna-mcp-server
|
|
30
|
+
kdna-mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Configure in your MCP client
|
|
34
|
+
|
|
35
|
+
**Claude App / Claude Code (`claude_desktop_config.json`):**
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"kdna": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["-y", "@aikdna/kdna-mcp-server"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Codex:**
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"kdna": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["-y", "@aikdna/kdna-mcp-server"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**OpenCode (`opencode.json`):**
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"kdna": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "@aikdna/kdna-mcp-server"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Environment Variables
|
|
75
|
+
|
|
76
|
+
| Variable | Purpose |
|
|
77
|
+
|----------|---------|
|
|
78
|
+
| `KDNA_ASSET_DIR` | Root directory for `kdna.available-local` (default: `~/.kdna/assets`) |
|
|
79
|
+
| `KDNA_REGISTRY_FILE` | Legacy path to `domains.json` for `kdna.available` |
|
|
80
|
+
| `KDNA_DATA_ROOT` | Override KDNA data directory (default: `~/.kdna`) |
|
|
81
|
+
|
|
82
|
+
## Local Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cd mcp-server
|
|
86
|
+
npm install
|
|
87
|
+
node bin/kdna-mcp.mjs
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Example: Verify and Load a Domain via MCP
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
# Client calls kdna.available-local { root: "./dist" }
|
|
94
|
+
# Client calls kdna.verify { assetPath: "./dist/writing.kdna" }
|
|
95
|
+
# Client calls kdna.load { assetPath: "./dist/writing.kdna", profile: "compact" }
|
|
96
|
+
# Agent injects prompt text into system context
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Relationship to kdna-loader skill
|
|
100
|
+
|
|
101
|
+
The MCP server and kdna-loader skill are complementary:
|
|
102
|
+
- **kdna-loader** = agents that can run CLI commands (Codex, Claude Code, OpenCode, Cursor)
|
|
103
|
+
- **MCP server** = any MCP-compatible runtime (including the above, plus custom agents)
|
|
104
|
+
|
|
105
|
+
For most users, the kdna-loader skill (installed by `kdna setup`) is the recommended path. Use the MCP server when you need programmatic, tool-based access.
|
package/bin/kdna-mcp.mjs
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import readline from 'node:readline';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const {
|
|
10
|
+
inspectKDNA,
|
|
11
|
+
loadKDNA,
|
|
12
|
+
matchDomain,
|
|
13
|
+
renderForAgent,
|
|
14
|
+
verifyAsset,
|
|
15
|
+
} = require('@aikdna/kdna-core');
|
|
16
|
+
const {
|
|
17
|
+
detectContainerFormat,
|
|
18
|
+
inspect: inspectV1,
|
|
19
|
+
isV1SourceDir,
|
|
20
|
+
loadV1,
|
|
21
|
+
validate: validateV1,
|
|
22
|
+
} = require('@aikdna/kdna-core/v1');
|
|
23
|
+
|
|
24
|
+
const tools = [
|
|
25
|
+
{
|
|
26
|
+
name: 'kdna.inspect',
|
|
27
|
+
description: 'Inspect a .kdna asset without extracting it.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
required: ['assetPath'],
|
|
31
|
+
properties: { assetPath: { type: 'string' }, verify: { type: 'boolean' } },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'kdna.verify',
|
|
36
|
+
description: 'Verify a .kdna asset integrity state.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
required: ['assetPath'],
|
|
40
|
+
properties: {
|
|
41
|
+
assetPath: { type: 'string' },
|
|
42
|
+
asset_digest: { type: 'string' },
|
|
43
|
+
content_digest: { type: 'string' },
|
|
44
|
+
requireSignature: { type: 'boolean' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'kdna.load',
|
|
50
|
+
description: 'Load a .kdna profile and return agent context.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
required: ['assetPath'],
|
|
54
|
+
properties: {
|
|
55
|
+
assetPath: { type: 'string' },
|
|
56
|
+
profile: { type: 'string', enum: ['index', 'compact', 'scenario', 'full'] },
|
|
57
|
+
input: { type: 'string' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'kdna.available-local',
|
|
63
|
+
description: 'List local v1 .kdna assets or v1 source directories without using a registry.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
root: { type: 'string' },
|
|
68
|
+
maxDepth: { type: 'number' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'kdna.match',
|
|
74
|
+
description: 'Rank .kdna assets for a task string.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
required: ['input', 'assetPaths'],
|
|
78
|
+
properties: {
|
|
79
|
+
input: { type: 'string' },
|
|
80
|
+
assetPaths: { type: 'array', items: { type: 'string' } },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'kdna.available',
|
|
86
|
+
description: 'Legacy: list assets from a local Registry domains.json file.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: { registryFile: { type: 'string' } },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
function send(id, result, error) {
|
|
95
|
+
const msg = error
|
|
96
|
+
? { jsonrpc: '2.0', id, error: { code: -32000, message: error.message || String(error) } }
|
|
97
|
+
: { jsonrpc: '2.0', id, result };
|
|
98
|
+
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function textResult(value) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isV1Asset(assetPath) {
|
|
108
|
+
if (!assetPath) return false;
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(assetPath) && fs.statSync(assetPath).isDirectory()) {
|
|
111
|
+
return isV1SourceDir(assetPath);
|
|
112
|
+
}
|
|
113
|
+
return detectContainerFormat(assetPath) === 'v1';
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function defaultAssetRoot() {
|
|
120
|
+
return process.env.KDNA_ASSET_DIR || path.join(os.homedir(), '.kdna', 'assets');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function findLocalAssets(root = defaultAssetRoot(), maxDepth = 3) {
|
|
124
|
+
if (!root || !fs.existsSync(root)) return [];
|
|
125
|
+
const found = [];
|
|
126
|
+
|
|
127
|
+
function visit(dir, depth) {
|
|
128
|
+
if (depth > maxDepth) return;
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isV1SourceDir(dir)) {
|
|
137
|
+
const inspection = inspectV1(dir);
|
|
138
|
+
const validation = validateV1(dir);
|
|
139
|
+
found.push({
|
|
140
|
+
path: dir,
|
|
141
|
+
kind: 'v1_source_dir',
|
|
142
|
+
asset_id: inspection.asset_id,
|
|
143
|
+
title: inspection.title,
|
|
144
|
+
version: inspection.version,
|
|
145
|
+
judgment_version: inspection.judgment_version,
|
|
146
|
+
checksums_present: Boolean(inspection.checksums_present),
|
|
147
|
+
loadable: Boolean(validation.overall_valid),
|
|
148
|
+
problems: validation.overall_valid ? [] : validation.problems || [],
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
155
|
+
const full = path.join(dir, entry.name);
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
visit(full, depth + 1);
|
|
158
|
+
} else if (entry.isFile() && entry.name.endsWith('.kdna') && detectContainerFormat(full) === 'v1') {
|
|
159
|
+
const inspection = inspectV1(full);
|
|
160
|
+
const validation = validateV1(full);
|
|
161
|
+
found.push({
|
|
162
|
+
path: full,
|
|
163
|
+
kind: 'v1_container',
|
|
164
|
+
asset_id: inspection.asset_id,
|
|
165
|
+
title: inspection.title,
|
|
166
|
+
version: inspection.version,
|
|
167
|
+
judgment_version: inspection.judgment_version,
|
|
168
|
+
checksums_present: Boolean(inspection.checksums_present),
|
|
169
|
+
loadable: Boolean(validation.overall_valid),
|
|
170
|
+
problems: validation.overall_valid ? [] : validation.problems || [],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
visit(path.resolve(root), 0);
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function listRegistry(registryFile) {
|
|
181
|
+
const file = registryFile || process.env.KDNA_REGISTRY_FILE;
|
|
182
|
+
if (!file) return [];
|
|
183
|
+
const registry = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
184
|
+
const domains = Array.isArray(registry.domains) ? registry.domains : Object.values(registry.domains || {});
|
|
185
|
+
return domains.map((d) => ({
|
|
186
|
+
legacy_registry: true,
|
|
187
|
+
name: d.name,
|
|
188
|
+
version: d.version,
|
|
189
|
+
asset_url: d.asset_url,
|
|
190
|
+
asset_digest: d.asset_digest,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function callTool(name, args = {}) {
|
|
195
|
+
if (name === 'kdna.inspect') {
|
|
196
|
+
if (isV1Asset(args.assetPath)) return textResult(inspectV1(args.assetPath));
|
|
197
|
+
return textResult(await inspectKDNA(args.assetPath, { verify: args.verify !== false }));
|
|
198
|
+
}
|
|
199
|
+
if (name === 'kdna.verify') {
|
|
200
|
+
if (isV1Asset(args.assetPath)) return textResult(validateV1(args.assetPath));
|
|
201
|
+
return textResult(await verifyAsset(args.assetPath, {
|
|
202
|
+
asset_digest: args.asset_digest,
|
|
203
|
+
content_digest: args.content_digest,
|
|
204
|
+
requireSignature: Boolean(args.requireSignature),
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
if (name === 'kdna.load') {
|
|
208
|
+
if (isV1Asset(args.assetPath)) {
|
|
209
|
+
const profile = args.profile || 'compact';
|
|
210
|
+
const loaded = loadV1(args.assetPath, { profile, as: 'json' });
|
|
211
|
+
const prompt = profile === 'index' ? null : loadV1(args.assetPath, { profile, as: 'prompt' }).text;
|
|
212
|
+
return textResult({ ...loaded, context: prompt });
|
|
213
|
+
}
|
|
214
|
+
const loaded = await loadKDNA(args.assetPath, { profile: args.profile || 'compact', input: args.input || '' });
|
|
215
|
+
const context = args.profile === 'index'
|
|
216
|
+
? null
|
|
217
|
+
: await renderForAgent(args.assetPath, { profile: args.profile || 'compact', input: args.input || '' });
|
|
218
|
+
return textResult({ ...loaded, context });
|
|
219
|
+
}
|
|
220
|
+
if (name === 'kdna.match') {
|
|
221
|
+
return textResult(await matchDomain(args.input || '', args.assetPaths || []));
|
|
222
|
+
}
|
|
223
|
+
if (name === 'kdna.available-local') {
|
|
224
|
+
return textResult(findLocalAssets(args.root, args.maxDepth || 3));
|
|
225
|
+
}
|
|
226
|
+
if (name === 'kdna.available') {
|
|
227
|
+
return textResult(listRegistry(args.registryFile));
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function handle(message) {
|
|
233
|
+
const { id, method, params = {} } = message;
|
|
234
|
+
if (method === 'initialize') {
|
|
235
|
+
send(id, {
|
|
236
|
+
protocolVersion: '2024-11-05',
|
|
237
|
+
capabilities: { tools: {} },
|
|
238
|
+
serverInfo: { name: '@aikdna/kdna-mcp-server', version: '0.1.0' },
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (method === 'tools/list') {
|
|
243
|
+
send(id, { tools });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (method === 'tools/call') {
|
|
247
|
+
send(id, await callTool(params.name, params.arguments || {}));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (id !== undefined) send(id, {});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
254
|
+
rl.on('line', async (line) => {
|
|
255
|
+
if (!line.trim()) return;
|
|
256
|
+
try {
|
|
257
|
+
await handle(JSON.parse(line));
|
|
258
|
+
} catch (e) {
|
|
259
|
+
send(undefined, null, e);
|
|
260
|
+
}
|
|
261
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aikdna/kdna-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for loading, inspecting, verifying, and matching KDNA .kdna assets.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kdna-mcp": "bin/kdna-mcp.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.mjs"
|
|
11
|
+
},
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@aikdna/kdna-core": "^0.11.1",
|
|
15
|
+
"ajv": "^8.20.0",
|
|
16
|
+
"ajv-formats": "^3.0.1"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { buildChecksumsV1 } = require('@aikdna/kdna-core/v1');
|
|
11
|
+
|
|
12
|
+
const server = path.join(process.cwd(), 'bin', 'kdna-mcp.mjs');
|
|
13
|
+
|
|
14
|
+
function callTool(name, args) {
|
|
15
|
+
const request = {
|
|
16
|
+
jsonrpc: '2.0',
|
|
17
|
+
id: 1,
|
|
18
|
+
method: 'tools/call',
|
|
19
|
+
params: { name, arguments: args },
|
|
20
|
+
};
|
|
21
|
+
const r = spawnSync(process.execPath, [server], {
|
|
22
|
+
input: `${JSON.stringify(request)}\n`,
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
});
|
|
25
|
+
assert.equal(r.status, 0, r.stderr);
|
|
26
|
+
const response = JSON.parse(r.stdout.trim());
|
|
27
|
+
assert.ok(!response.error, response.error && response.error.message);
|
|
28
|
+
return JSON.parse(response.result.content[0].text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeV1Source(root) {
|
|
32
|
+
const dir = path.join(root, 'writing');
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
fs.writeFileSync(path.join(dir, 'mimetype'), 'application/vnd.kdna.asset');
|
|
35
|
+
fs.writeFileSync(path.join(dir, 'kdna.json'), JSON.stringify({
|
|
36
|
+
kdna_version: '1.0',
|
|
37
|
+
asset_id: 'kdna:test:writing',
|
|
38
|
+
asset_uid: 'urn:uuid:11111111-1111-4111-8111-111111111111',
|
|
39
|
+
asset_type: 'domain',
|
|
40
|
+
title: 'Test Writing',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
judgment_version: '1.0.0',
|
|
43
|
+
created_at: '2026-06-18T00:00:00.000Z',
|
|
44
|
+
updated_at: '2026-06-18T00:00:00.000Z',
|
|
45
|
+
creator: { name: 'MCP Test', id: 'mcp-test' },
|
|
46
|
+
lineage: { type: 'original', fork_of: null, derived_from: null },
|
|
47
|
+
payload: { path: 'payload.kdnab', encoding: 'json', encrypted: false },
|
|
48
|
+
compatibility: { min_loader_version: '1.0.0', profile: 'judgment-profile-v1' },
|
|
49
|
+
load_contract: {
|
|
50
|
+
default_profile: 'compact',
|
|
51
|
+
profiles: {
|
|
52
|
+
index: { requires_decryption: false, max_tokens_hint: 200 },
|
|
53
|
+
compact: { requires_decryption: false, max_tokens_hint: 2000 },
|
|
54
|
+
scenario: { requires_decryption: false, selection: 'triggered_sections_only' },
|
|
55
|
+
full: { requires_decryption: false, intended_for: ['audit'] },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}, null, 2));
|
|
59
|
+
fs.writeFileSync(path.join(dir, 'payload.kdnab'), JSON.stringify({
|
|
60
|
+
profile: 'judgment-profile-v1',
|
|
61
|
+
core: {
|
|
62
|
+
highest_question: 'What makes this writing judgment useful?',
|
|
63
|
+
axioms: [{ id: 'ax1', one_sentence: 'Structure before wording.' }],
|
|
64
|
+
boundaries: [{ type: 'stance_boundary', stance: 'Do not polish before diagnosis.' }],
|
|
65
|
+
risk_model: {},
|
|
66
|
+
},
|
|
67
|
+
patterns: [],
|
|
68
|
+
scenarios: [],
|
|
69
|
+
cases: [],
|
|
70
|
+
reasoning: { self_checks: ['Did I diagnose before editing?'], failure_modes: [] },
|
|
71
|
+
}, null, 2));
|
|
72
|
+
fs.writeFileSync(path.join(dir, 'checksums.json'), JSON.stringify(buildChecksumsV1(dir), null, 2));
|
|
73
|
+
return dir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test('available-local discovers v1 source dirs and load returns prompt context', () => {
|
|
77
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-mcp-v1-'));
|
|
78
|
+
try {
|
|
79
|
+
makeV1Source(root);
|
|
80
|
+
const available = callTool('kdna.available-local', { root, maxDepth: 1 });
|
|
81
|
+
assert.equal(available.length, 1);
|
|
82
|
+
assert.equal(available[0].asset_id, 'kdna:test:writing');
|
|
83
|
+
assert.equal(available[0].loadable, true, JSON.stringify(available[0]));
|
|
84
|
+
assert.equal(Object.prototype.hasOwnProperty.call(available[0], 'quality_badge'), false);
|
|
85
|
+
|
|
86
|
+
const loaded = callTool('kdna.load', { assetPath: path.join(root, 'writing'), profile: 'compact' });
|
|
87
|
+
assert.equal(loaded.asset_id, 'kdna:test:writing');
|
|
88
|
+
assert.match(loaded.context, /Structure before wording/);
|
|
89
|
+
assert.match(loaded.context, /Do not polish before diagnosis/);
|
|
90
|
+
} finally {
|
|
91
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|