keepra-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.
Files changed (3) hide show
  1. package/README.md +57 -0
  2. package/keepra-mcp.js +290 -0
  3. package/package.json +34 -0
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # keepra-mcp
2
+
3
+ One-command [MCP](https://modelcontextprotocol.io) server that connects **Claude, ChatGPT, Cursor, Windsurf, and any MCP-compatible AI** to your local **[Keepra](https://keepra.tech)** app — tasks, notes, links, contacts, and (with per-item grants) vault entries.
4
+
5
+ Access is **scoped, revocable, and 100% local**: the server only talks to the Keepra desktop app on `127.0.0.1`, and every call is gated by a key you create inside Keepra (Settings → AI Access) where you choose exactly what it can reach.
6
+
7
+ ## Prerequisites
8
+
9
+ - The **Keepra desktop app** installed and running (it hosts the local API the server talks to).
10
+ - An **AI Access key** from Keepra → **Settings → AI Access → New Key** (looks like `kp_…`).
11
+
12
+ ## Use it (no install needed — `npx`)
13
+
14
+ **Claude Code:**
15
+
16
+ ```bash
17
+ claude mcp add keepra --env KEEPRA_KEY=kp_your_key_here -- npx -y keepra-mcp
18
+ ```
19
+
20
+ **Claude Desktop / Cursor / Windsurf** — add to the client's MCP config:
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "keepra": {
26
+ "command": "npx",
27
+ "args": ["-y", "keepra-mcp"],
28
+ "env": {
29
+ "KEEPRA_KEY": "kp_your_key_here"
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ That's it — no file paths, no manual download. `npx` fetches and runs the latest server.
37
+
38
+ ## Environment variables
39
+
40
+ | Var | Default | Notes |
41
+ |-----|---------|-------|
42
+ | `KEEPRA_KEY` | *(required)* | Your scoped AI Access key (`kp_…`) from Keepra. |
43
+ | `KEEPRA_URL` | `http://127.0.0.1:47615` | Local Keepra API endpoint. Only change if you've remapped the port. |
44
+
45
+ ## Available tools
46
+
47
+ `list_tasks`, `add_task`, `complete_task`, `list_notes`, `read_note`, `create_note`, `update_note`, `list_links`, `add_link`, `list_contacts`, `get_contact`, `add_contact`, `list_files`, plus per-item vault grants (`list_credentials`, `get_credential`) and optional `run_command` / FTP tools — each enabled only if your key allows it.
48
+
49
+ ## Security
50
+
51
+ - The key is **device-local** (created in Keepra, never synced) and **revocable** any time.
52
+ - The server binds to **localhost only** — no cloud relay, no external origin.
53
+ - Keepra enforces the key's scope on every request; the AI can't reach anything you didn't grant.
54
+
55
+ ---
56
+
57
+ Part of [Keepra](https://keepra.tech) · Issues: <https://github.com/mudassir-awan/keepra/issues>
package/keepra-mcp.js ADDED
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+ /* ═══════════════════════════════════════════════════════════════════════════
3
+ Keepra MCP server — connects AI clients (Claude Desktop, Claude Code,
4
+ Cursor, ChatGPT via an MCP bridge, …) to the Keepra desktop app.
5
+
6
+ Transport: MCP stdio (newline-delimited JSON-RPC 2.0). Zero dependencies.
7
+
8
+ Config (Claude Desktop example — %APPDATA%\Claude\claude_desktop_config.json):
9
+ {
10
+ "mcpServers": {
11
+ "keepra": {
12
+ "command": "node",
13
+ "args": ["C:\\Keepra\\keepra-mcp.js"],
14
+ "env": { "KEEPRA_KEY": "kp_xxxxxxxx" }
15
+ }
16
+ }
17
+ }
18
+
19
+ The key is created in Keepra → Settings → AI Access, where you choose
20
+ EXACTLY what it can reach (tasks / notes / links read/write, and individual
21
+ vault items). This server only ever exposes the tools that key allows —
22
+ everything else is invisible to the AI. The Keepra desktop app must be
23
+ running (it hosts the local API on 127.0.0.1:47615).
24
+ ═══════════════════════════════════════════════════════════════════════════ */
25
+ 'use strict';
26
+ const http = require('http');
27
+
28
+ const KEY = process.env.KEEPRA_KEY || '';
29
+ const BASE = process.env.KEEPRA_URL || 'http://127.0.0.1:47615';
30
+
31
+ function api(action, params) {
32
+ return new Promise((resolve) => {
33
+ const data = JSON.stringify({ action, params: params || {} });
34
+ const u = new URL('/api/mcp', BASE);
35
+ const req = http.request({
36
+ hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'Content-Length': Buffer.byteLength(data),
40
+ 'Authorization': 'Bearer ' + KEY,
41
+ },
42
+ }, (res) => {
43
+ let b = '';
44
+ res.on('data', (c) => { b += c; });
45
+ res.on('end', () => { try { resolve(JSON.parse(b)); } catch { resolve({ error: 'bad response from Keepra' }); } });
46
+ });
47
+ req.on('error', () => resolve({ error: 'The Keepra desktop app is not running. Ask the user to launch Keepra, then try again.' }));
48
+ req.end(data);
49
+ });
50
+ }
51
+
52
+ /* Tool catalogue — each tool maps 1:1 to a bridge action and is only listed
53
+ when the key's scopes allow it. */
54
+ const TOOLS = [
55
+ { scope: 'tasks:read', def: { name: 'list_tasks',
56
+ description: "List the user's to-dos in Keepra Tasks (title, due date, list, completion, steps).",
57
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false } } },
58
+ { scope: 'tasks:write', def: { name: 'add_task',
59
+ description: 'Add a to-do to Keepra Tasks.',
60
+ inputSchema: { type: 'object', properties: {
61
+ title: { type: 'string', description: 'Task title' },
62
+ due: { type: 'string', description: 'Due date YYYY-MM-DD (optional)' },
63
+ important: { type: 'boolean' }, myDay: { type: 'boolean', description: 'Add to My Day' },
64
+ note: { type: 'string' }, list: { type: 'string', description: 'Target list name (optional)' },
65
+ priority: { type: 'string', enum: ['high', 'med', 'low'] },
66
+ }, required: ['title'] } } },
67
+ { scope: 'tasks:write', def: { name: 'complete_task',
68
+ description: 'Mark a Keepra task completed, by task id or exact title.',
69
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Task id or exact title' } }, required: ['id'] } } },
70
+
71
+ { scope: 'notes:read', def: { name: 'list_notes',
72
+ description: "List the user's Keepra notes (id, title, tags, modified, short preview).",
73
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false } } },
74
+ { scope: 'notes:read', def: { name: 'read_note',
75
+ description: 'Read the full markdown content of one Keepra note, by id or exact title.',
76
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Note id or exact title' } }, required: ['id'] } } },
77
+ { scope: 'notes:write', def: { name: 'create_note',
78
+ description: 'Create a new markdown note in Keepra.',
79
+ inputSchema: { type: 'object', properties: {
80
+ title: { type: 'string' }, content: { type: 'string', description: 'Markdown body' },
81
+ tags: { type: 'array', items: { type: 'string' } },
82
+ } } } },
83
+
84
+ { scope: 'links:read', def: { name: 'list_links',
85
+ description: "List the user's saved links/bookmarks in Keepra (title, url, category, tags).",
86
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false } } },
87
+ { scope: 'links:write', def: { name: 'add_link',
88
+ description: 'Save a new link/bookmark in Keepra.',
89
+ inputSchema: { type: 'object', properties: {
90
+ url: { type: 'string' }, title: { type: 'string' },
91
+ category: { type: 'string' }, description: { type: 'string' },
92
+ tags: { type: 'array', items: { type: 'string' } },
93
+ }, required: ['url'] } } },
94
+
95
+ { scope: 'contacts:read', def: { name: 'list_contacts',
96
+ description: "List the user's Keepra contacts (name, company, role, phones, emails, links, tags).",
97
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false } } },
98
+ { scope: 'contacts:read', def: { name: 'get_contact',
99
+ description: 'Get one Keepra contact with all phones, emails and links, by id or name.',
100
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Contact id or (partial) name' } }, required: ['id'] } } },
101
+ { scope: 'contacts:write', def: { name: 'add_contact',
102
+ description: 'Add a person to Keepra Contacts. Phones/emails/urls are arrays of {label, value} rows (e.g. {"label":"Business","value":"+82 ..."}).',
103
+ inputSchema: { type: 'object', properties: {
104
+ name: { type: 'string', description: 'Full name' },
105
+ company: { type: 'string' }, role: { type: 'string' },
106
+ phones: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } }, required: ['value'] } },
107
+ emails: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } }, required: ['value'] } },
108
+ urls: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } }, required: ['value'] }, description: 'Websites, portfolio, social links' },
109
+ tags: { type: 'array', items: { type: 'string' } },
110
+ notes: { type: 'string' },
111
+ }, required: ['name'] } } },
112
+
113
+ { scope: 'files:read', def: { name: 'list_files',
114
+ description: "List the user's stored docs in Keepra Files (name, size, kind, cloud status) — metadata only, file content is never exposed.",
115
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false } } },
116
+
117
+ { scope: 'vault', def: { name: 'list_credentials',
118
+ description: 'List the vault credentials this key was granted (names and types only — no secrets).',
119
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false } } },
120
+ { scope: 'vault', def: { name: 'get_credential',
121
+ description: 'Get metadata for a granted vault credential. SECRET VALUES ARE NEVER RETURNED TO AI — Keepra strips all values and returns only environment variable names (e.g. KV_HOSTINGER_FTP_PASS). Use run_command with env_from_vault to run commands with secrets auto-injected.',
122
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Credential id or exact title' } }, required: ['id'] } } },
123
+ { scope: 'vault', def: { name: 'run_command',
124
+ description: 'Execute a shell command on the user\'s PC with vault secrets injected as environment variables. Secrets are fetched locally by Keepra and injected into the child process — the AI never sees the values. stdout/stderr are returned with all secret values redacted. Env var key names must be UPPERCASE_SNAKE_CASE (max 50 chars); system variables (PATH, NODE_PATH, SHELL, etc.) cannot be overridden. Max 20 env_from_vault entries. cwd must be an existing directory. Timeout: 30s.',
125
+ inputSchema: { type: 'object',
126
+ properties: {
127
+ command: { type: 'string', description: 'Shell command to execute (max 2000 chars). Reference injected secrets by env var name, e.g. node deploy.js or curl -u %FTP_USER%:%FTP_PASS% ...' },
128
+ cwd: { type: 'string', description: 'Absolute path to working directory (must already exist; defaults to C:\\Keepra)' },
129
+ env_from_vault: {
130
+ type: 'object',
131
+ description: 'Map of UPPERCASE_ENV_VAR → "vaultItemId.fieldKey". Max 20 entries. Example: {"FTP_PASS": "mqew8vq28jmv.ftpPass", "FTP_USER": "mqew8vq28jmv.ftpUser"}',
132
+ additionalProperties: { type: 'string' },
133
+ },
134
+ },
135
+ required: ['command'] } } },
136
+
137
+ { scope: 'vault', def: {
138
+ name: 'ftp_list',
139
+ description: 'List files and directories on the FTP server. Uses vault credentials internally — no password is exposed to the AI.',
140
+ inputSchema: { type: 'object', properties: {
141
+ vault_item_id: { type: 'string', description: 'Vault item ID containing FTP credentials' },
142
+ path: { type: 'string', description: 'Remote path to list (default: current directory)', default: '/' },
143
+ }, required: ['vault_item_id'] } } },
144
+
145
+ { scope: 'vault', def: {
146
+ name: 'ftp_upload',
147
+ description: 'Upload a local file to the FTP server. Uses vault credentials internally.',
148
+ inputSchema: { type: 'object', properties: {
149
+ vault_item_id: { type: 'string', description: 'Vault item ID containing FTP credentials' },
150
+ local_path: { type: 'string', description: 'Absolute local file path to upload' },
151
+ remote_path: { type: 'string', description: 'Remote path/filename to upload to' },
152
+ }, required: ['vault_item_id', 'local_path', 'remote_path'] } } },
153
+
154
+ { scope: 'vault', def: {
155
+ name: 'ftp_download',
156
+ description: 'Download a file from the FTP server to local disk. Uses vault credentials internally.',
157
+ inputSchema: { type: 'object', properties: {
158
+ vault_item_id: { type: 'string', description: 'Vault item ID containing FTP credentials' },
159
+ remote_path: { type: 'string', description: 'Remote file path to download' },
160
+ local_path: { type: 'string', description: 'Local path to save the downloaded file' },
161
+ }, required: ['vault_item_id', 'remote_path', 'local_path'] } } },
162
+
163
+ { scope: 'vault', def: {
164
+ name: 'ftp_delete',
165
+ description: 'Delete a file or empty directory on the FTP server. REQUIRES explicit confirmation parameter — set confirmed:true to proceed.',
166
+ inputSchema: { type: 'object', properties: {
167
+ vault_item_id: { type: 'string', description: 'Vault item ID containing FTP credentials' },
168
+ remote_path: { type: 'string', description: 'Remote path to delete' },
169
+ confirmed: { type: 'boolean', description: 'Must be explicitly set to true to confirm deletion' },
170
+ }, required: ['vault_item_id', 'remote_path', 'confirmed'] } } },
171
+
172
+ { scope: 'vault', def: {
173
+ name: 'ftp_mkdir',
174
+ description: 'Create a directory on the FTP server. Uses vault credentials internally.',
175
+ inputSchema: { type: 'object', properties: {
176
+ vault_item_id: { type: 'string', description: 'Vault item ID containing FTP credentials' },
177
+ path: { type: 'string', description: 'Remote directory path to create' },
178
+ }, required: ['vault_item_id', 'path'] } } },
179
+
180
+ { scope: 'vault', def: {
181
+ name: 'ftp_rename',
182
+ description: 'Rename or move a file/directory on the FTP server. Uses vault credentials internally.',
183
+ inputSchema: { type: 'object', properties: {
184
+ vault_item_id: { type: 'string', description: 'Vault item ID containing FTP credentials' },
185
+ old_path: { type: 'string', description: 'Current remote path' },
186
+ new_path: { type: 'string', description: 'New remote path' },
187
+ }, required: ['vault_item_id', 'old_path', 'new_path'] } } },
188
+ ];
189
+
190
+ let scopeCache = null; // { scopes: string[], allowRunCommand: boolean }
191
+ let scopeCacheTime = 0;
192
+ const SCOPE_CACHE_TTL = 5 * 60 * 1000; // 5 minutes — ensures revoked keys take effect promptly
193
+
194
+ async function fetchScopes() {
195
+ if (!scopeCache || Date.now() - scopeCacheTime > SCOPE_CACHE_TTL) {
196
+ const r = await api('scopes');
197
+ if (r && r.ok) { scopeCache = { scopes: r.result.scopes || [], allowRunCommand: !!r.result.allowRunCommand }; scopeCacheTime = Date.now(); }
198
+ else return { error: (r && r.error) || 'could not reach Keepra' };
199
+ }
200
+ return null; // null = success
201
+ }
202
+
203
+ async function allowedTools() {
204
+ const err = await fetchScopes();
205
+ if (err) return err;
206
+ const hasVault = scopeCache.scopes.some((s) => s.startsWith('vault:item:'));
207
+ const tools = TOOLS
208
+ .filter((t) => (t.scope === 'vault' ? hasVault : scopeCache.scopes.includes(t.scope)))
209
+ .map((t) => t.def);
210
+ let allowedTools = scopeCache.allowRunCommand ? tools : tools.filter((t) => t.name !== 'run_command');
211
+ const hasFtpScope = (scopeCache.scopes || []).some((s) => s.startsWith('vault:item:'));
212
+ if (!hasFtpScope) allowedTools = allowedTools.filter((t) => !t.name.startsWith('ftp_'));
213
+ return allowedTools;
214
+ }
215
+
216
+ /* ── stdio JSON-RPC loop ── */
217
+ function send(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); }
218
+
219
+ async function onMessage(msg) {
220
+ const reply = (result) => send({ jsonrpc: '2.0', id: msg.id, result });
221
+ const replyErr = (code, message) => send({ jsonrpc: '2.0', id: msg.id, error: { code, message } });
222
+
223
+ switch (msg.method) {
224
+ case 'initialize':
225
+ return reply({
226
+ protocolVersion: (msg.params && msg.params.protocolVersion) || '2024-11-05',
227
+ capabilities: { tools: {} },
228
+ serverInfo: { name: 'keepra', version: '1.0.0' },
229
+ });
230
+ case 'notifications/initialized':
231
+ case 'notifications/cancelled':
232
+ return; // notifications get no response
233
+ case 'ping':
234
+ return reply({});
235
+ case 'tools/list': {
236
+ const tools = await allowedTools();
237
+ if (tools.error) return reply({ tools: [], _error: tools.error });
238
+ return reply({ tools });
239
+ }
240
+ case 'tools/call': {
241
+ const name = msg.params && msg.params.name;
242
+ const args = (msg.params && msg.params.arguments) || {};
243
+ if (!TOOLS.some((t) => t.def.name === name)) return replyErr(-32602, 'unknown tool: ' + name);
244
+ if (name === 'run_command') {
245
+ await fetchScopes();
246
+ if (!scopeCache?.allowRunCommand) {
247
+ return reply({ content: [{ type: 'text', text: JSON.stringify({ error: 'run_command is not enabled for this key — open Keepra → Settings → AI Access, edit the key, and enable "Allow shell command execution"' }) }], isError: true });
248
+ }
249
+ }
250
+ if (['ftp_list', 'ftp_upload', 'ftp_download', 'ftp_delete', 'ftp_mkdir', 'ftp_rename'].includes(name)) {
251
+ if (name === 'ftp_delete' && !args.confirmed) {
252
+ return reply({ content: [{ type: 'text', text: JSON.stringify({ error: 'ftp_delete requires confirmed:true to proceed. This is a destructive operation.' }) }], isError: true });
253
+ }
254
+ await fetchScopes();
255
+ if (!args.vault_item_id) {
256
+ return reply({ content: [{ type: 'text', text: JSON.stringify({ error: 'vault_item_id is required' }) }], isError: true });
257
+ }
258
+ const ftpRes = await api(name, { vault_item_id: args.vault_item_id, ...args });
259
+ if (!ftpRes || !ftpRes.ok) {
260
+ return reply({ content: [{ type: 'text', text: JSON.stringify({ error: ftpRes?.error || 'FTP operation failed' }) }], isError: true });
261
+ }
262
+ return reply({ content: [{ type: 'text', text: JSON.stringify(ftpRes.result) }] });
263
+ }
264
+ const r = await api(name, args);
265
+ if (r && r.ok) return reply({ content: [{ type: 'text', text: JSON.stringify(r.result, null, 2) }] });
266
+ return reply({ content: [{ type: 'text', text: 'Error: ' + ((r && r.error) || 'unknown error') }], isError: true });
267
+ }
268
+ default:
269
+ if (msg.id !== undefined) return replyErr(-32601, 'method not found: ' + msg.method);
270
+ }
271
+ }
272
+
273
+ let buf = '';
274
+ process.stdin.setEncoding('utf8');
275
+ process.stdin.on('data', (chunk) => {
276
+ buf += chunk;
277
+ let i;
278
+ while ((i = buf.indexOf('\n')) >= 0) {
279
+ const line = buf.slice(0, i).trim();
280
+ buf = buf.slice(i + 1);
281
+ if (!line) continue;
282
+ try { onMessage(JSON.parse(line)); } catch { /* ignore malformed line */ }
283
+ }
284
+ });
285
+ process.stdin.on('end', () => process.exit(0));
286
+
287
+ if (!KEY) {
288
+ // Surface a clear setup error to the client log without crashing the handshake.
289
+ process.stderr.write('keepra-mcp: KEEPRA_KEY env var is not set — create a key in Keepra → Settings → AI Access\n');
290
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "keepra-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that connects Claude, ChatGPT, Cursor, Windsurf and any MCP client to your local Keepra app (tasks, notes, links, contacts, vault) with scoped, revocable, device-local keys.",
5
+ "bin": {
6
+ "keepra-mcp": "keepra-mcp.js"
7
+ },
8
+ "main": "keepra-mcp.js",
9
+ "files": [
10
+ "keepra-mcp.js",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=16"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "keepra",
19
+ "model-context-protocol",
20
+ "claude",
21
+ "chatgpt",
22
+ "cursor",
23
+ "windsurf",
24
+ "ai",
25
+ "productivity"
26
+ ],
27
+ "homepage": "https://keepra.tech",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/mudassir-awan/keepra.git"
31
+ },
32
+ "author": "IBRANICS / Keepra",
33
+ "license": "MIT"
34
+ }