@kanecta/mcp 1.0.0 → 1.0.1
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/package.json +1 -1
- package/src/index.js +114 -97
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const {
|
|
8
|
-
const { walkDataDir } = require('@kanecta/lib/src/datastore');
|
|
7
|
+
const { Datastore } = require('@kanecta/lib');
|
|
9
8
|
|
|
10
9
|
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
11
10
|
|
|
@@ -20,13 +19,12 @@ function writeConfig(cfg) {
|
|
|
20
19
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + '\n');
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
function
|
|
24
|
-
if (process.env.KANECTA_DATASTORE) {
|
|
25
|
-
return process.env.KANECTA_DATASTORE.replace(/^~/, os.homedir());
|
|
26
|
-
}
|
|
22
|
+
function openDs() {
|
|
27
23
|
const cfg = readConfig();
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const p = cfg?.datastorePath
|
|
25
|
+
? cfg.datastorePath.replace(/^~/, os.homedir())
|
|
26
|
+
: (process.env.KANECTA_DATASTORE?.replace(/^~/, os.homedir()) ?? DEFAULT_DATASTORE_PATH);
|
|
27
|
+
return { ds: new Datastore(p), cfg };
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
// ─── Secret detection ─────────────────────────────────────────────────────────
|
|
@@ -56,6 +54,7 @@ const TOOLS = [
|
|
|
56
54
|
type: 'object',
|
|
57
55
|
properties: {
|
|
58
56
|
text: { type: 'string', description: 'The content to capture' },
|
|
57
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags (e.g. ["decision", "architecture"])' },
|
|
59
58
|
type: { type: 'string', enum: ['text', 'string', 'decision'], description: 'Item type — defaults to "text"' },
|
|
60
59
|
},
|
|
61
60
|
required: ['text'],
|
|
@@ -85,13 +84,13 @@ const TOOLS = [
|
|
|
85
84
|
},
|
|
86
85
|
{
|
|
87
86
|
name: 'kanecta_get',
|
|
88
|
-
description: 'Get a specific item from the knowledge base by UUID.',
|
|
87
|
+
description: 'Get a specific item from the knowledge base by UUID or alias.',
|
|
89
88
|
inputSchema: {
|
|
90
89
|
type: 'object',
|
|
91
90
|
properties: {
|
|
92
|
-
|
|
91
|
+
ref: { type: 'string', description: 'Item UUID or alias' },
|
|
93
92
|
},
|
|
94
|
-
required: ['
|
|
93
|
+
required: ['ref'],
|
|
95
94
|
},
|
|
96
95
|
},
|
|
97
96
|
{
|
|
@@ -110,10 +109,10 @@ const TOOLS = [
|
|
|
110
109
|
inputSchema: {
|
|
111
110
|
type: 'object',
|
|
112
111
|
properties: {
|
|
113
|
-
|
|
112
|
+
ref: { type: 'string', description: 'Root item UUID or alias' },
|
|
114
113
|
depth: { type: 'number', description: 'Depth to expand (default: 3)' },
|
|
115
114
|
},
|
|
116
|
-
required: ['
|
|
115
|
+
required: ['ref'],
|
|
117
116
|
},
|
|
118
117
|
},
|
|
119
118
|
{
|
|
@@ -125,7 +124,7 @@ const TOOLS = [
|
|
|
125
124
|
value: { type: 'string', description: 'Item value/content' },
|
|
126
125
|
type: { type: 'string', description: 'Item type (string, text, object, etc.)' },
|
|
127
126
|
parentId: { type: 'string', description: 'Parent UUID — omit for root' },
|
|
128
|
-
|
|
127
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
|
|
129
128
|
},
|
|
130
129
|
},
|
|
131
130
|
},
|
|
@@ -138,6 +137,7 @@ const TOOLS = [
|
|
|
138
137
|
id: { type: 'string', description: 'Item UUID' },
|
|
139
138
|
value: { type: 'string', description: 'New value/content' },
|
|
140
139
|
type: { type: 'string', description: 'New type' },
|
|
140
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Replace tags' },
|
|
141
141
|
},
|
|
142
142
|
required: ['id'],
|
|
143
143
|
},
|
|
@@ -149,7 +149,6 @@ const TOOLS = [
|
|
|
149
149
|
type: 'object',
|
|
150
150
|
properties: {
|
|
151
151
|
id: { type: 'string', description: 'Item UUID' },
|
|
152
|
-
force: { type: 'boolean', description: 'Delete even if other items link to this one' },
|
|
153
152
|
},
|
|
154
153
|
required: ['id'],
|
|
155
154
|
},
|
|
@@ -158,78 +157,80 @@ const TOOLS = [
|
|
|
158
157
|
|
|
159
158
|
// ─── Handlers ─────────────────────────────────────────────────────────────────
|
|
160
159
|
|
|
161
|
-
|
|
162
|
-
const
|
|
160
|
+
function ensureDateBucket(ds, cfg) {
|
|
161
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
162
|
+
if (cfg?.lastCaptureDate === today && cfg?.lastCaptureDateId) {
|
|
163
|
+
return cfg.lastCaptureDateId;
|
|
164
|
+
}
|
|
165
|
+
const bucket = ds.create({
|
|
166
|
+
value: today,
|
|
167
|
+
type: 'string',
|
|
168
|
+
parentId: cfg?.capturesRootId || null,
|
|
169
|
+
owner: cfg?.owner || ds.config.owner,
|
|
170
|
+
tags: ['kanecta-date'],
|
|
171
|
+
});
|
|
172
|
+
ds.setAlias(`kanecta-date-${today}`, bucket.id);
|
|
173
|
+
if (cfg) {
|
|
174
|
+
cfg.lastCaptureDate = today;
|
|
175
|
+
cfg.lastCaptureDateId = bucket.id;
|
|
176
|
+
writeConfig(cfg);
|
|
177
|
+
}
|
|
178
|
+
return bucket.id;
|
|
179
|
+
}
|
|
163
180
|
|
|
181
|
+
function handleCapture(args, ds, cfg) {
|
|
182
|
+
const { text, tags = [], type = 'text' } = args;
|
|
164
183
|
const secrets = detectSecrets(text);
|
|
165
184
|
if (secrets.length) {
|
|
166
185
|
return { error: `Capture rejected — possible secret detected (${secrets.join(', ')}). Kanecta never stores secrets.` };
|
|
167
186
|
}
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
let dateBucketId;
|
|
173
|
-
if (cfg?.lastCaptureDate === today && cfg?.lastCaptureDateId) {
|
|
174
|
-
dateBucketId = cfg.lastCaptureDateId;
|
|
175
|
-
} else {
|
|
176
|
-
const bucket = await connector.addItem({
|
|
177
|
-
value: today,
|
|
178
|
-
type: 'string',
|
|
179
|
-
parentId: cfg?.capturesRootId || null,
|
|
180
|
-
owner: cfg?.owner || 'kanecta',
|
|
181
|
-
});
|
|
182
|
-
dateBucketId = bucket.id;
|
|
183
|
-
if (cfg) {
|
|
184
|
-
cfg.lastCaptureDate = today;
|
|
185
|
-
cfg.lastCaptureDateId = bucket.id;
|
|
186
|
-
writeConfig(cfg);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const item = await connector.addItem({
|
|
187
|
+
const dateBucketId = ensureDateBucket(ds, cfg);
|
|
188
|
+
const allTags = ['kanecta-capture', ...tags.filter(t => !['kanecta-capture', 'kanecta-date', 'kanecta-internal'].includes(t))];
|
|
189
|
+
const item = ds.create({
|
|
191
190
|
value: text,
|
|
192
191
|
type,
|
|
193
192
|
parentId: dateBucketId,
|
|
194
|
-
owner: cfg?.owner ||
|
|
193
|
+
owner: cfg?.owner || ds.config.owner,
|
|
194
|
+
tags: allTags,
|
|
195
195
|
});
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
return {
|
|
197
|
+
id: item.id,
|
|
198
|
+
date: new Date().toISOString().slice(0, 10),
|
|
199
|
+
tags: allTags.filter(t => t !== 'kanecta-capture'),
|
|
200
|
+
preview: text.slice(0, 120),
|
|
201
|
+
};
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
|
|
204
|
+
function handleSearch(args, ds) {
|
|
201
205
|
const { query, limit = 10 } = args;
|
|
202
206
|
const q = query.toLowerCase();
|
|
203
|
-
const
|
|
204
|
-
const results = all
|
|
207
|
+
const results = ds.loadAll()
|
|
205
208
|
.filter(i => i.value && typeof i.value === 'string' && i.value.toLowerCase().includes(q))
|
|
206
|
-
.sort((a, b) => (b.
|
|
209
|
+
.sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''))
|
|
207
210
|
.slice(0, limit)
|
|
208
|
-
.map(i => ({
|
|
211
|
+
.map(i => ({
|
|
212
|
+
id: i.id,
|
|
213
|
+
type: i.type,
|
|
214
|
+
tags: (i.tags || []).filter(t => !['kanecta-capture', 'kanecta-date', 'kanecta-internal'].includes(t)),
|
|
215
|
+
date: (i.createdAt || '').slice(0, 10),
|
|
216
|
+
value: i.value,
|
|
217
|
+
}));
|
|
209
218
|
return { query, count: results.length, results };
|
|
210
219
|
}
|
|
211
220
|
|
|
212
|
-
|
|
221
|
+
function handleRecent(args, ds) {
|
|
213
222
|
const { n = 10 } = args;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
218
|
-
const dateBuckets = new Map(
|
|
219
|
-
all.filter(i => typeof i.value === 'string' && datePattern.test(i.value)).map(i => [i.id, i.value])
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
const captures = all
|
|
223
|
-
.filter(i => i.parentId && dateBuckets.has(i.parentId))
|
|
224
|
-
.map(i => ({ ...i, _date: dateBuckets.get(i.parentId) }))
|
|
225
|
-
.sort((a, b) => {
|
|
226
|
-
if (b._date !== a._date) return b._date.localeCompare(a._date);
|
|
227
|
-
return (b.sortOrder || 0) - (a.sortOrder || 0);
|
|
228
|
-
})
|
|
223
|
+
const items = ds.loadAll()
|
|
224
|
+
.filter(i => (i.tags || []).includes('kanecta-capture'))
|
|
225
|
+
.sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''))
|
|
229
226
|
.slice(0, n)
|
|
230
|
-
.map(
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
.map(i => ({
|
|
228
|
+
id: i.id,
|
|
229
|
+
date: (i.createdAt || '').slice(0, 10),
|
|
230
|
+
tags: (i.tags || []).filter(t => !['kanecta-capture', 'kanecta-date', 'kanecta-internal'].includes(t)),
|
|
231
|
+
value: i.value,
|
|
232
|
+
}));
|
|
233
|
+
return { count: items.length, items };
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
// ─── MCP protocol ─────────────────────────────────────────────────────────────
|
|
@@ -246,17 +247,38 @@ function sendError(id, code, message) {
|
|
|
246
247
|
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
|
|
250
|
+
function dispatch(name, args) {
|
|
251
|
+
const { ds, cfg } = openDs();
|
|
250
252
|
switch (name) {
|
|
251
|
-
case 'kanecta_capture': return handleCapture(args,
|
|
252
|
-
case 'kanecta_search': return handleSearch(args,
|
|
253
|
-
case 'kanecta_recent': return handleRecent(args,
|
|
254
|
-
case 'kanecta_get':
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
case '
|
|
259
|
-
|
|
253
|
+
case 'kanecta_capture': return handleCapture(args, ds, cfg);
|
|
254
|
+
case 'kanecta_search': return handleSearch(args, ds);
|
|
255
|
+
case 'kanecta_recent': return handleRecent(args, ds);
|
|
256
|
+
case 'kanecta_get': {
|
|
257
|
+
const item = ds.resolve(args.ref);
|
|
258
|
+
return item || { error: `Not found: ${args.ref}` };
|
|
259
|
+
}
|
|
260
|
+
case 'kanecta_get_children':
|
|
261
|
+
return { items: ds.children(args.parentId ?? null) };
|
|
262
|
+
case 'kanecta_get_tree': {
|
|
263
|
+
const root = ds.resolve(args.ref);
|
|
264
|
+
if (!root) return { error: `Not found: ${args.ref}` };
|
|
265
|
+
return {
|
|
266
|
+
count: 0,
|
|
267
|
+
tree: ds.tree(root.id, args.depth ?? 3).map(({ item, depth }) => ({
|
|
268
|
+
depth, id: item.id, value: item.value, type: item.type,
|
|
269
|
+
tags: (item.tags || []).filter(t => t !== 'kanecta-internal'),
|
|
270
|
+
})),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
case 'kanecta_add_item':
|
|
274
|
+
return ds.create(args);
|
|
275
|
+
case 'kanecta_update_item': {
|
|
276
|
+
const { id, ...changes } = args;
|
|
277
|
+
return ds.update(id, changes, cfg?.owner);
|
|
278
|
+
}
|
|
279
|
+
case 'kanecta_delete_item':
|
|
280
|
+
ds.delete(args.id, cfg?.owner);
|
|
281
|
+
return { deleted: args.id };
|
|
260
282
|
default: {
|
|
261
283
|
const err = new Error(`Unknown tool: ${name}`);
|
|
262
284
|
err.code = -32601;
|
|
@@ -266,9 +288,6 @@ async function dispatch(name, args, connector, datastorePath) {
|
|
|
266
288
|
}
|
|
267
289
|
|
|
268
290
|
function runMcpServer() {
|
|
269
|
-
const datastorePath = getDatastorePath();
|
|
270
|
-
const connector = new KanectaConnector({ datastorePath });
|
|
271
|
-
|
|
272
291
|
let buf = '';
|
|
273
292
|
process.stdin.setEncoding('utf8');
|
|
274
293
|
process.stdin.on('data', chunk => {
|
|
@@ -305,23 +324,21 @@ function runMcpServer() {
|
|
|
305
324
|
|
|
306
325
|
if (method === 'tools/call') {
|
|
307
326
|
const { name, arguments: args = {} } = params;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
});
|
|
327
|
+
let result;
|
|
328
|
+
try {
|
|
329
|
+
result = dispatch(name, args);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (err.code === -32601) {
|
|
332
|
+
sendError(id, -32601, err.message);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
sendResult(id, { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const text = result?.error
|
|
339
|
+
? `Error: ${result.error}`
|
|
340
|
+
: JSON.stringify(result, null, 2);
|
|
341
|
+
sendResult(id, { content: [{ type: 'text', text }], isError: !!result?.error });
|
|
325
342
|
continue;
|
|
326
343
|
}
|
|
327
344
|
|