@kanecta/mcp 1.0.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,129 @@
1
+ # @kanecta/mcp
2
+
3
+ MCP (Model Context Protocol) server for [Kanecta](https://github.com/cloudsculptor/kanecta) — gives Claude direct, structured access to your personal knowledge base.
4
+
5
+ Once installed, Claude can capture insights, search past context, and browse your knowledge base as native tools — no slash commands, no prompting required.
6
+
7
+ ---
8
+
9
+ ## Quick start
10
+
11
+ ### Install via Claude Code CLI (recommended)
12
+
13
+ ```bash
14
+ claude mcp add --transport stdio kanecta -- npx -y @kanecta/mcp
15
+ ```
16
+
17
+ ### Install via Kanecta wizard
18
+
19
+ If you have `@kanecta/claude` installed, the setup wizard handles this automatically:
20
+
21
+ ```bash
22
+ kanecta claude wizard
23
+ ```
24
+
25
+ ### Install manually (Claude Desktop)
26
+
27
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "kanecta": {
33
+ "command": "npx",
34
+ "args": ["-y", "@kanecta/mcp"],
35
+ "env": {
36
+ "KANECTA_DATASTORE": "/path/to/your/kanecta/datastore"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### Install manually (Claude Code `settings.json`)
44
+
45
+ Add to `~/.claude/settings.json`:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "kanecta": {
51
+ "command": "npx",
52
+ "args": ["-y", "@kanecta/mcp"],
53
+ "type": "stdio"
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Datastore discovery
62
+
63
+ The server resolves the datastore path in this order:
64
+
65
+ 1. `KANECTA_DATASTORE` environment variable
66
+ 2. `~/.kanecta-config.json` → `datastorePath` (set by `kanecta claude wizard`)
67
+ 3. Default: `~/.kanecta/`
68
+
69
+ ---
70
+
71
+ ## Tools
72
+
73
+ | Tool | Description |
74
+ |------|-------------|
75
+ | `kanecta_capture` | Save context, decisions, or insights. Never accepts secrets. |
76
+ | `kanecta_search` | Full-text substring search across all items. |
77
+ | `kanecta_recent` | List the most recent captures. |
78
+ | `kanecta_get` | Fetch a specific item by UUID. |
79
+ | `kanecta_get_children` | List children of an item (omit `parentId` for roots). |
80
+ | `kanecta_get_tree` | Get an item with its subtree expanded to a given depth. |
81
+ | `kanecta_add_item` | Add an item with explicit placement in the hierarchy. |
82
+ | `kanecta_update_item` | Update an item's value or type. |
83
+ | `kanecta_delete_item` | Delete an item (pass `force: true` to override backlink check). |
84
+
85
+ ---
86
+
87
+ ## How captures are organised
88
+
89
+ Captures are grouped under date items (`YYYY-MM-DD`) in the hierarchy:
90
+
91
+ ```
92
+ Claude Captures
93
+ └── 2025-05-16
94
+ ├── "Decided to use PostgreSQL for the sessions table"
95
+ └── "Auth middleware rewrite is driven by compliance, not tech debt"
96
+ └── 2025-05-15
97
+ └── "Merge freeze begins 2025-05-22 for mobile release"
98
+ ```
99
+
100
+ `kanecta_recent` returns the most recent captures sorted by date then insertion order.
101
+
102
+ ---
103
+
104
+ ## Secret protection
105
+
106
+ `kanecta_capture` refuses to store content that matches known secret patterns (API keys, tokens, private keys, passwords). This runs client-side — nothing is sent to any external service.
107
+
108
+ ---
109
+
110
+ ## Requirements
111
+
112
+ - Node.js ≥ 18
113
+ - A Kanecta datastore (created by `kanecta claude wizard` or `@kanecta/lib`)
114
+
115
+ ---
116
+
117
+ ## Checking server status
118
+
119
+ Inside a Claude Code session:
120
+
121
+ ```
122
+ /mcp
123
+ ```
124
+
125
+ ---
126
+
127
+ ## License
128
+
129
+ MIT
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@kanecta/mcp",
3
+ "version": "1.0.0",
4
+ "description": "Kanecta MCP server — gives Claude direct access to your personal knowledge base",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "kanecta-mcp": "src/index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js",
11
+ "release": "bash scripts/release.sh",
12
+ "release:minor": "bash scripts/release.sh minor",
13
+ "release:major": "bash scripts/release.sh major"
14
+ },
15
+ "files": [
16
+ "src/",
17
+ "scripts/"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "dependencies": {
23
+ "@kanecta/lib": "^1.0.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "keywords": [
29
+ "kanecta",
30
+ "mcp",
31
+ "model-context-protocol",
32
+ "claude",
33
+ "claude-code",
34
+ "knowledge-base",
35
+ "personal-memory",
36
+ "ai",
37
+ "notes"
38
+ ],
39
+ "author": "Richie Thomas <richardsempire@gmail.com>",
40
+ "license": "MIT"
41
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ BUMP=${1:-patch}
5
+
6
+ if [[ "$BUMP" != "patch" && "$BUMP" != "minor" && "$BUMP" != "major" ]]; then
7
+ echo "Usage: npm run release [-- patch|minor|major] (default: patch)"
8
+ exit 1
9
+ fi
10
+
11
+ cd "$(dirname "$0")/.."
12
+
13
+ echo "==> Checking for uncommitted changes..."
14
+ if ! git diff --quiet || ! git diff --cached --quiet; then
15
+ echo "Error: uncommitted changes detected. Commit or stash them first."
16
+ exit 1
17
+ fi
18
+
19
+ echo "==> Checking npm auth..."
20
+ if ! npm whoami &>/dev/null; then
21
+ echo "Error: not logged in to npm. Run: npm login"
22
+ exit 1
23
+ fi
24
+
25
+ echo "==> Bumping $BUMP version..."
26
+ npm version "$BUMP" --message "chore(kanecta-mcp): release v%s"
27
+
28
+ echo "==> Publishing to npm..."
29
+ npm publish --access public
30
+
31
+ echo "==> Pushing commit and tag..."
32
+ git push --follow-tags
33
+
34
+ NEW_VERSION=$(node -p "require('./package.json').version")
35
+ echo ""
36
+ echo "Released @kanecta/mcp v$NEW_VERSION"
package/src/index.js ADDED
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const os = require('os');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { KanectaConnector } = require('@kanecta/lib');
8
+ const { walkDataDir } = require('@kanecta/lib/src/datastore');
9
+
10
+ // ─── Config ───────────────────────────────────────────────────────────────────
11
+
12
+ const CONFIG_PATH = path.join(os.homedir(), '.kanecta-config.json');
13
+ const DEFAULT_DATASTORE_PATH = path.join(os.homedir(), '.kanecta');
14
+
15
+ function readConfig() {
16
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch { return null; }
17
+ }
18
+
19
+ function writeConfig(cfg) {
20
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + '\n');
21
+ }
22
+
23
+ function getDatastorePath() {
24
+ if (process.env.KANECTA_DATASTORE) {
25
+ return process.env.KANECTA_DATASTORE.replace(/^~/, os.homedir());
26
+ }
27
+ const cfg = readConfig();
28
+ if (cfg && cfg.datastorePath) return cfg.datastorePath.replace(/^~/, os.homedir());
29
+ return DEFAULT_DATASTORE_PATH;
30
+ }
31
+
32
+ // ─── Secret detection ─────────────────────────────────────────────────────────
33
+
34
+ const SECRET_PATTERNS = [
35
+ { name: 'Anthropic API key', re: /sk-ant-[a-zA-Z0-9_-]{20,}/ },
36
+ { name: 'OpenAI API key', re: /sk-[a-zA-Z0-9]{20,}/ },
37
+ { name: 'AWS access key', re: /AKIA[0-9A-Z]{16}/ },
38
+ { name: 'GitHub token', re: /gh[psoure]_[a-zA-Z0-9]{36,}/ },
39
+ { name: 'JWT', re: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/ },
40
+ { name: 'private key', re: /-----BEGIN [A-Z ]+ PRIVATE KEY-----/ },
41
+ { name: 'secret/password field', re: /(password|passwd|secret|api[_-]?key|private[_-]?key|access[_-]?token)\s*[=:]\s*\S{8,}/i },
42
+ ];
43
+
44
+ function detectSecrets(text) {
45
+ if (!text || typeof text !== 'string') return [];
46
+ return SECRET_PATTERNS.filter(({ re }) => re.test(text)).map(({ name }) => name);
47
+ }
48
+
49
+ // ─── Tools ────────────────────────────────────────────────────────────────────
50
+
51
+ const TOOLS = [
52
+ {
53
+ name: 'kanecta_capture',
54
+ description: 'Save context, decisions, insights, or facts to the Kanecta knowledge base. Use for anything worth remembering across sessions. Never call this with secrets, API keys, passwords, or tokens.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ text: { type: 'string', description: 'The content to capture' },
59
+ type: { type: 'string', enum: ['text', 'string', 'decision'], description: 'Item type — defaults to "text"' },
60
+ },
61
+ required: ['text'],
62
+ },
63
+ },
64
+ {
65
+ name: 'kanecta_search',
66
+ description: 'Search the Kanecta knowledge base for past context, decisions, or facts. Case-insensitive substring match across all item values. Use before starting complex work to check for relevant prior context.',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ query: { type: 'string', description: 'Search query' },
71
+ limit: { type: 'number', description: 'Maximum results (default: 10)' },
72
+ },
73
+ required: ['query'],
74
+ },
75
+ },
76
+ {
77
+ name: 'kanecta_recent',
78
+ description: 'List the most recent captures from the knowledge base.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ n: { type: 'number', description: 'Number of items to return (default: 10)' },
83
+ },
84
+ },
85
+ },
86
+ {
87
+ name: 'kanecta_get',
88
+ description: 'Get a specific item from the knowledge base by UUID.',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ id: { type: 'string', description: 'Item UUID' },
93
+ },
94
+ required: ['id'],
95
+ },
96
+ },
97
+ {
98
+ name: 'kanecta_get_children',
99
+ description: 'Get the direct children of an item. Omit parentId to list root-level items.',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: {
103
+ parentId: { type: 'string', description: 'Parent UUID — omit for root items' },
104
+ },
105
+ },
106
+ },
107
+ {
108
+ name: 'kanecta_get_tree',
109
+ description: 'Get an item and its subtree expanded to a given depth.',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ id: { type: 'string', description: 'Root item UUID' },
114
+ depth: { type: 'number', description: 'Depth to expand (default: 3)' },
115
+ },
116
+ required: ['id'],
117
+ },
118
+ },
119
+ {
120
+ name: 'kanecta_add_item',
121
+ description: 'Add a new item to the knowledge base with explicit placement. Use kanecta_capture for saving insights — this is for structured data entry.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ value: { type: 'string', description: 'Item value/content' },
126
+ type: { type: 'string', description: 'Item type (string, text, object, etc.)' },
127
+ parentId: { type: 'string', description: 'Parent UUID — omit for root' },
128
+ sortOrder: { type: 'number', description: 'Sort position (auto-assigned if omitted)' },
129
+ },
130
+ },
131
+ },
132
+ {
133
+ name: 'kanecta_update_item',
134
+ description: 'Update an existing item in the knowledge base.',
135
+ inputSchema: {
136
+ type: 'object',
137
+ properties: {
138
+ id: { type: 'string', description: 'Item UUID' },
139
+ value: { type: 'string', description: 'New value/content' },
140
+ type: { type: 'string', description: 'New type' },
141
+ },
142
+ required: ['id'],
143
+ },
144
+ },
145
+ {
146
+ name: 'kanecta_delete_item',
147
+ description: 'Delete an item from the knowledge base.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ id: { type: 'string', description: 'Item UUID' },
152
+ force: { type: 'boolean', description: 'Delete even if other items link to this one' },
153
+ },
154
+ required: ['id'],
155
+ },
156
+ },
157
+ ];
158
+
159
+ // ─── Handlers ─────────────────────────────────────────────────────────────────
160
+
161
+ async function handleCapture(args, connector) {
162
+ const { text, type = 'text' } = args;
163
+
164
+ const secrets = detectSecrets(text);
165
+ if (secrets.length) {
166
+ return { error: `Capture rejected — possible secret detected (${secrets.join(', ')}). Kanecta never stores secrets.` };
167
+ }
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({
191
+ value: text,
192
+ type,
193
+ parentId: dateBucketId,
194
+ owner: cfg?.owner || 'kanecta',
195
+ });
196
+
197
+ return { id: item.id, date: today, preview: text.slice(0, 120) };
198
+ }
199
+
200
+ async function handleSearch(args, datastorePath) {
201
+ const { query, limit = 10 } = args;
202
+ const q = query.toLowerCase();
203
+ const all = await walkDataDir(datastorePath);
204
+ const results = all
205
+ .filter(i => i.value && typeof i.value === 'string' && i.value.toLowerCase().includes(q))
206
+ .sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0))
207
+ .slice(0, limit)
208
+ .map(i => ({ id: i.id, type: i.type, parentId: i.parentId, value: i.value }));
209
+ return { query, count: results.length, results };
210
+ }
211
+
212
+ async function handleRecent(args, datastorePath) {
213
+ 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
+ })
229
+ .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 };
233
+ }
234
+
235
+ // ─── MCP protocol ─────────────────────────────────────────────────────────────
236
+
237
+ function send(msg) {
238
+ process.stdout.write(JSON.stringify(msg) + '\n');
239
+ }
240
+
241
+ function sendResult(id, result) {
242
+ send({ jsonrpc: '2.0', id, result });
243
+ }
244
+
245
+ function sendError(id, code, message) {
246
+ send({ jsonrpc: '2.0', id, error: { code, message } });
247
+ }
248
+
249
+ async function dispatch(name, args, connector, datastorePath) {
250
+ 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 }));
260
+ default: {
261
+ const err = new Error(`Unknown tool: ${name}`);
262
+ err.code = -32601;
263
+ throw err;
264
+ }
265
+ }
266
+ }
267
+
268
+ function runMcpServer() {
269
+ const datastorePath = getDatastorePath();
270
+ const connector = new KanectaConnector({ datastorePath });
271
+
272
+ let buf = '';
273
+ process.stdin.setEncoding('utf8');
274
+ process.stdin.on('data', chunk => {
275
+ buf += chunk;
276
+ let nl;
277
+ while ((nl = buf.indexOf('\n')) !== -1) {
278
+ const line = buf.slice(0, nl).trim();
279
+ buf = buf.slice(nl + 1);
280
+ if (!line) continue;
281
+
282
+ let msg;
283
+ try { msg = JSON.parse(line); } catch {
284
+ send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
285
+ continue;
286
+ }
287
+
288
+ const { id, method, params = {} } = msg;
289
+
290
+ if (method === 'initialize') {
291
+ sendResult(id, {
292
+ protocolVersion: '2024-11-05',
293
+ capabilities: { tools: {} },
294
+ serverInfo: { name: 'kanecta', version: require('../package.json').version },
295
+ });
296
+ continue;
297
+ }
298
+
299
+ if (method === 'notifications/initialized') continue;
300
+
301
+ if (method === 'tools/list') {
302
+ sendResult(id, { tools: TOOLS });
303
+ continue;
304
+ }
305
+
306
+ if (method === 'tools/call') {
307
+ 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
+ });
325
+ continue;
326
+ }
327
+
328
+ sendError(id, -32601, `Method not found: ${method}`);
329
+ }
330
+ });
331
+
332
+ process.stdin.on('end', () => process.exit(0));
333
+ }
334
+
335
+ module.exports = { runMcpServer, TOOLS };
336
+
337
+ if (require.main === module) {
338
+ runMcpServer();
339
+ }