@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +114 -97
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanecta/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Kanecta MCP server — gives Claude direct access to your personal knowledge base",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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 { KanectaConnector } = require('@kanecta/lib');
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 getDatastorePath() {
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
- if (cfg && cfg.datastorePath) return cfg.datastorePath.replace(/^~/, os.homedir());
29
- return DEFAULT_DATASTORE_PATH;
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
- id: { type: 'string', description: 'Item UUID' },
91
+ ref: { type: 'string', description: 'Item UUID or alias' },
93
92
  },
94
- required: ['id'],
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
- id: { type: 'string', description: 'Root item UUID' },
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: ['id'],
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
- sortOrder: { type: 'number', description: 'Sort position (auto-assigned if omitted)' },
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
- async function handleCapture(args, connector) {
162
- const { text, type = 'text' } = args;
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 cfg = readConfig();
170
- const today = new Date().toISOString().slice(0, 10);
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 || 'kanecta',
193
+ owner: cfg?.owner || ds.config.owner,
194
+ tags: allTags,
195
195
  });
196
-
197
- return { id: item.id, date: today, preview: text.slice(0, 120) };
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
- async function handleSearch(args, datastorePath) {
204
+ function handleSearch(args, ds) {
201
205
  const { query, limit = 10 } = args;
202
206
  const q = query.toLowerCase();
203
- const all = await walkDataDir(datastorePath);
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.sortOrder || 0) - (a.sortOrder || 0))
209
+ .sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''))
207
210
  .slice(0, limit)
208
- .map(i => ({ id: i.id, type: i.type, parentId: i.parentId, value: i.value }));
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
- async function handleRecent(args, datastorePath) {
221
+ function handleRecent(args, ds) {
213
222
  const { n = 10 } = args;
214
- const all = await walkDataDir(datastorePath);
215
-
216
- // Captures live under date bucket items (value = YYYY-MM-DD)
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(({ _date, ...i }) => ({ id: i.id, type: i.type, date: _date, value: i.value }));
231
-
232
- return { count: captures.length, items: captures };
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
- async function dispatch(name, args, connector, datastorePath) {
250
+ function dispatch(name, args) {
251
+ const { ds, cfg } = openDs();
250
252
  switch (name) {
251
- case 'kanecta_capture': return handleCapture(args, connector);
252
- case 'kanecta_search': return handleSearch(args, datastorePath);
253
- case 'kanecta_recent': return handleRecent(args, datastorePath);
254
- case 'kanecta_get': return connector.getItem(args.id);
255
- case 'kanecta_get_children': return connector.getChildren(args.parentId ?? null);
256
- case 'kanecta_get_tree': return connector.getTree(args.id, { depth: args.depth ?? 3 });
257
- case 'kanecta_add_item': return connector.addItem(args);
258
- case 'kanecta_update_item': { const { id, ...updates } = args; return connector.updateItem(id, updates); }
259
- case 'kanecta_delete_item': return connector.deleteItem(args.id, { force: args.force ?? false }).then(() => ({ deleted: args.id }));
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
- dispatch(name, args, connector, datastorePath)
309
- .then(result => {
310
- const text = result.error
311
- ? `Error: ${result.error}`
312
- : JSON.stringify(result, null, 2);
313
- sendResult(id, { content: [{ type: 'text', text }], isError: !!result.error });
314
- })
315
- .catch(err => {
316
- if (err.code === -32601) {
317
- sendError(id, -32601, err.message);
318
- } else {
319
- sendResult(id, {
320
- content: [{ type: 'text', text: `Error: ${err.message}` }],
321
- isError: true,
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