@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 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.
@@ -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
+ });