@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 +129 -0
- package/package.json +41 -0
- package/scripts/release.sh +36 -0
- package/src/index.js +339 -0
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
|
+
}
|