ziphro-mcp 0.0.0-stage โ†’ 2.0.0-preview.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 CHANGED
@@ -1,3 +1,90 @@
1
- # Temporary Holding Version
1
+ # ziphro-mcp ๐Ÿช
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Model Context Protocol (MCP) server for Ziphro: a privacy-first task manager with client-side end-to-end zero-knowledge encryption.
4
+
5
+ This MCP server allows AI agents (like Claude Desktop, Cursor, Windsurf, Cline, etc.) to securely read, update, complete, and analyze your tasks without exposing master passwords or sensitive credentials.
6
+
7
+ ---
8
+
9
+ ## ๐Ÿš€ Quick Setup (Browser Pairing)
10
+
11
+ Authorize your AI agent in one click:
12
+ ```bash
13
+ npx -y ziphro-mcp auth
14
+ ```
15
+
16
+ This starts a local handshake receiver and opens your browser to authorize the agent. An isolated zero-knowledge API Secret (`zs_live_...`) is derived and saved to `~/.ziphro/mcp.json`. Master passwords are **never** required, requested, or stored.
17
+
18
+ ---
19
+
20
+ ## ๐Ÿ› ๏ธ Running the Server
21
+
22
+ Run directly via `npx` (uses `~/.ziphro/mcp.json` automatically):
23
+ ```bash
24
+ npx -y ziphro-mcp
25
+ ```
26
+
27
+ Or install globally:
28
+ ```bash
29
+ npm install -g ziphro-mcp
30
+ ziphro-mcp
31
+ ```
32
+
33
+ ---
34
+
35
+ ## โš™๏ธ Client Configuration
36
+
37
+ ### Claude Desktop
38
+ Add this to your `claude_desktop_config.json`:
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "ziphro": {
43
+ "command": "npx",
44
+ "args": ["-y", "ziphro-mcp"]
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ Or specify environment variables explicitly:
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "ziphro": {
55
+ "command": "npx",
56
+ "args": ["-y", "ziphro-mcp"],
57
+ "env": {
58
+ "ZIPHRO_API_KEY": "zp_live_<your-api-key>",
59
+ "ZIPHRO_API_SECRET": "zs_live_<your-api-secret>",
60
+ "ZIPHRO_API": "https://api-ziphro.vercel.app"
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Cursor IDE
68
+ 1. Open Cursor Settings -> Features -> MCP.
69
+ 2. Click **+ Add New MCP Server**.
70
+ 3. Command: `npx -y ziphro-mcp` (zero-config after `ziphro-mcp auth`).
71
+
72
+ ---
73
+
74
+ ## ๐Ÿ”‘ Available Tools
75
+
76
+ * **`list_tasks`**: List user tasks with optional status (`pending`, `in_progress`, `completed`), priority (`low`, `medium`, `high`), and tag filters.
77
+ * **`add_task`**: Create a new task with description, priority, due date, and tags.
78
+ * **`complete_task`**: Mark a task as completed with an optional note.
79
+ * **`get_task_stats`**: Get aggregated task completion metrics and dashboard data.
80
+ * **`suggest_priorities`**: Ranks active/pending tasks by priority and urgency.
81
+ * **`search_tasks`**: Full-text search tasks by keyword, tag, or due date range.
82
+ * **`update_task`**: Update description, priority, status, tags, or due date.
83
+ * **`bulk_complete`**: Mark multiple tasks as completed in a single action.
84
+ * **`get_overdue`**: Retrieve all overdue tasks past their deadlines.
85
+
86
+ ---
87
+
88
+ ## ๐Ÿ“„ License
89
+
90
+ MIT License.
package/dist/index.js ADDED
@@ -0,0 +1,1171 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ErrorCode,
9
+ ListToolsRequestSchema,
10
+ McpError
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+
13
+ // ../packages/crypto/dist/web.js
14
+ var KEY_LENGTH = 32;
15
+ var PBKDF2_ITERATIONS_V2 = 6e5;
16
+ var PBKDF2_ITERATIONS_V1 = 1e5;
17
+ function uint8ArrayToBase64(arr) {
18
+ const binString = Array.from(arr, (byte) => String.fromCharCode(byte)).join("");
19
+ return btoa(binString);
20
+ }
21
+ function base64ToUint8Array(str) {
22
+ const binString = atob(str);
23
+ return Uint8Array.from(binString, (char) => char.charCodeAt(0));
24
+ }
25
+ function getSubtleCrypto() {
26
+ const c = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
27
+ if (!c || !c.subtle) {
28
+ throw new Error("Web Crypto Subtle is not available in this environment.");
29
+ }
30
+ return c.subtle;
31
+ }
32
+ function getRandomValues(arr) {
33
+ const c = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
34
+ return c.getRandomValues(arr);
35
+ }
36
+ function toBufferSource(bytes) {
37
+ if (bytes.buffer instanceof ArrayBuffer) {
38
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
39
+ }
40
+ return new Uint8Array(bytes);
41
+ }
42
+ async function deriveAccountKeys(passphrase, emailSalt) {
43
+ const subtle = getSubtleCrypto();
44
+ const encoder = new TextEncoder();
45
+ const normalizedSalt = encoder.encode(emailSalt.trim().toLowerCase());
46
+ const passphraseBytes = encoder.encode(passphrase);
47
+ const pbkdf2Key = await subtle.importKey("raw", passphraseBytes, { name: "PBKDF2" }, false, ["deriveBits"]);
48
+ const masterBits = await subtle.deriveBits({
49
+ name: "PBKDF2",
50
+ salt: normalizedSalt,
51
+ iterations: PBKDF2_ITERATIONS_V2,
52
+ hash: "SHA-256"
53
+ }, pbkdf2Key, KEY_LENGTH * 8);
54
+ const hkdfKey = await subtle.importKey("raw", masterBits, "HKDF", false, ["deriveBits"]);
55
+ const authBits = await subtle.deriveBits({
56
+ name: "HKDF",
57
+ hash: "SHA-256",
58
+ salt: new Uint8Array(0),
59
+ info: encoder.encode("ziphro-auth-v2")
60
+ }, hkdfKey, KEY_LENGTH * 8);
61
+ const encBits = await subtle.deriveBits({
62
+ name: "HKDF",
63
+ hash: "SHA-256",
64
+ salt: new Uint8Array(0),
65
+ info: encoder.encode("ziphro-encryption-v2")
66
+ }, hkdfKey, KEY_LENGTH * 8);
67
+ return {
68
+ authSecret: uint8ArrayToBase64(new Uint8Array(authBits)),
69
+ encKey: new Uint8Array(encBits)
70
+ };
71
+ }
72
+ function buildAADBytes(meta) {
73
+ return new TextEncoder().encode(JSON.stringify({
74
+ u: meta.userId,
75
+ r: meta.revision,
76
+ v: meta.version ?? 2
77
+ }));
78
+ }
79
+ async function encryptWithAAD(plaintext, encKey, meta) {
80
+ const subtle = getSubtleCrypto();
81
+ const iv = getRandomValues(new Uint8Array(12));
82
+ const salt = getRandomValues(new Uint8Array(16));
83
+ const aad = buildAADBytes(meta);
84
+ const cryptoKey = ArrayBuffer.isView(encKey) ? await subtle.importKey("raw", toBufferSource(encKey), { name: "AES-GCM" }, false, ["encrypt"]) : encKey;
85
+ const plaintextBytes = new TextEncoder().encode(plaintext);
86
+ const encryptedBuffer = await subtle.encrypt({
87
+ name: "AES-GCM",
88
+ iv: toBufferSource(iv),
89
+ additionalData: toBufferSource(aad),
90
+ tagLength: 128
91
+ }, cryptoKey, plaintextBytes);
92
+ const encryptedBytes = new Uint8Array(encryptedBuffer);
93
+ const ciphertextBytes = encryptedBytes.slice(0, -16);
94
+ const authTagBytes = encryptedBytes.slice(-16);
95
+ return {
96
+ version: "2",
97
+ iterations: PBKDF2_ITERATIONS_V2,
98
+ revision: meta.revision,
99
+ salt: uint8ArrayToBase64(salt),
100
+ iv: uint8ArrayToBase64(iv),
101
+ ciphertext: uint8ArrayToBase64(ciphertextBytes),
102
+ authTag: uint8ArrayToBase64(authTagBytes)
103
+ };
104
+ }
105
+ async function decryptWithAAD(blob, encKey, meta) {
106
+ const subtle = getSubtleCrypto();
107
+ const iv = base64ToUint8Array(blob.iv);
108
+ const ciphertextBytes = base64ToUint8Array(blob.ciphertext);
109
+ const authTagBytes = base64ToUint8Array(blob.authTag);
110
+ const combined = new Uint8Array(ciphertextBytes.length + authTagBytes.length);
111
+ combined.set(ciphertextBytes, 0);
112
+ combined.set(authTagBytes, ciphertextBytes.length);
113
+ const cryptoKey = ArrayBuffer.isView(encKey) ? await subtle.importKey("raw", toBufferSource(encKey), { name: "AES-GCM" }, false, ["decrypt"]) : encKey;
114
+ const gcmParams = {
115
+ name: "AES-GCM",
116
+ iv: toBufferSource(iv),
117
+ tagLength: 128
118
+ };
119
+ if (blob.version === "2" && meta) {
120
+ gcmParams.additionalData = toBufferSource(buildAADBytes({
121
+ userId: meta.userId,
122
+ revision: meta.revision,
123
+ version: meta.version ?? 2
124
+ }));
125
+ }
126
+ const decryptedBuffer = await subtle.decrypt(gcmParams, cryptoKey, combined);
127
+ return new TextDecoder().decode(decryptedBuffer);
128
+ }
129
+ async function legacyDecrypt(blob, password, meta) {
130
+ const subtle = getSubtleCrypto();
131
+ const encoder = new TextEncoder();
132
+ const salt = base64ToUint8Array(blob.salt);
133
+ const iv = base64ToUint8Array(blob.iv);
134
+ const ciphertextBytes = base64ToUint8Array(blob.ciphertext);
135
+ const authTagBytes = base64ToUint8Array(blob.authTag);
136
+ const combined = new Uint8Array(ciphertextBytes.length + authTagBytes.length);
137
+ combined.set(ciphertextBytes, 0);
138
+ combined.set(authTagBytes, ciphertextBytes.length);
139
+ const isV2 = blob.version === "2";
140
+ const iterations = isV2 ? blob.iterations || PBKDF2_ITERATIONS_V2 : PBKDF2_ITERATIONS_V1;
141
+ const passwordBytes = encoder.encode(password);
142
+ const pbkdf2Key = await subtle.importKey("raw", passwordBytes, { name: "PBKDF2" }, false, ["deriveBits"]);
143
+ const masterBits = await subtle.deriveBits({
144
+ name: "PBKDF2",
145
+ salt: toBufferSource(salt),
146
+ iterations,
147
+ hash: "SHA-256"
148
+ }, pbkdf2Key, KEY_LENGTH * 8);
149
+ let cryptoKey;
150
+ if (isV2) {
151
+ const hkdfKey = await subtle.importKey("raw", masterBits, "HKDF", false, ["deriveKey"]);
152
+ cryptoKey = await subtle.deriveKey({
153
+ name: "HKDF",
154
+ hash: "SHA-256",
155
+ salt: new Uint8Array(0),
156
+ info: encoder.encode("ziphro-encryption")
157
+ }, hkdfKey, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
158
+ } else {
159
+ const labelBytes = encoder.encode("ziphro-encryption");
160
+ const combinedKey = new Uint8Array(masterBits.byteLength + labelBytes.length);
161
+ combinedKey.set(new Uint8Array(masterBits), 0);
162
+ combinedKey.set(labelBytes, masterBits.byteLength);
163
+ const hashBuffer = await subtle.digest("SHA-256", combinedKey);
164
+ cryptoKey = await subtle.importKey("raw", hashBuffer, { name: "AES-GCM" }, false, ["decrypt"]);
165
+ }
166
+ const gcmParams = {
167
+ name: "AES-GCM",
168
+ iv: toBufferSource(iv),
169
+ tagLength: 128
170
+ };
171
+ if (isV2 && meta) {
172
+ try {
173
+ const decryptedWithAAD = await subtle.decrypt({
174
+ ...gcmParams,
175
+ additionalData: toBufferSource(buildAADBytes(meta))
176
+ }, cryptoKey, combined);
177
+ return new TextDecoder().decode(decryptedWithAAD);
178
+ } catch {
179
+ }
180
+ }
181
+ const decryptedBuffer = await subtle.decrypt(gcmParams, cryptoKey, combined);
182
+ return new TextDecoder().decode(decryptedBuffer);
183
+ }
184
+ async function checksum(plaintext) {
185
+ const subtle = getSubtleCrypto();
186
+ const data = new TextEncoder().encode(plaintext);
187
+ const hashBuffer = await subtle.digest("SHA-256", data);
188
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
189
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
190
+ }
191
+
192
+ // src/crypto.ts
193
+ function normalizeKeyOrSecret(secretOrPass) {
194
+ if (secretOrPass instanceof Uint8Array) return secretOrPass;
195
+ if (!secretOrPass || typeof secretOrPass !== "string") return new Uint8Array(32);
196
+ const trimmed = secretOrPass.trim();
197
+ if (trimmed.startsWith("zs_live_")) {
198
+ const hex = trimmed.slice("zs_live_".length);
199
+ if (hex.length === 64) {
200
+ return new Uint8Array(Buffer.from(hex, "hex"));
201
+ }
202
+ }
203
+ if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
204
+ return new Uint8Array(Buffer.from(trimmed, "hex"));
205
+ }
206
+ if (/^[A-Za-z0-9+/]{43}=?$/.test(trimmed) || /^[A-Za-z0-9+/]{44}$/.test(trimmed)) {
207
+ const buf = Buffer.from(trimmed, "base64");
208
+ if (buf.length === 32) {
209
+ return new Uint8Array(buf);
210
+ }
211
+ }
212
+ return trimmed;
213
+ }
214
+ async function encrypt(plaintext, secretOrPass, meta) {
215
+ const normalized = normalizeKeyOrSecret(secretOrPass);
216
+ let encKey;
217
+ if (typeof normalized === "string") {
218
+ const salt = meta?.userId || "ziphro-mcp-user";
219
+ const keys = await deriveAccountKeys(normalized, salt);
220
+ encKey = keys.encKey;
221
+ } else {
222
+ encKey = normalized;
223
+ }
224
+ const envelope = meta || {
225
+ userId: "mcp",
226
+ revision: 1,
227
+ version: 2
228
+ };
229
+ return encryptWithAAD(plaintext, encKey, envelope);
230
+ }
231
+ async function decrypt(blob, secretOrPass, meta) {
232
+ const normalized = normalizeKeyOrSecret(secretOrPass);
233
+ if (normalized instanceof Uint8Array) {
234
+ const candidateUserIds = [
235
+ meta?.userId,
236
+ "mcp",
237
+ "default",
238
+ "cli"
239
+ ].filter((u) => Boolean(u));
240
+ let lastErr;
241
+ for (const uid of candidateUserIds) {
242
+ try {
243
+ return await decryptWithAAD(blob, normalized, {
244
+ userId: uid,
245
+ revision: meta?.revision ?? 1,
246
+ version: meta?.version ?? 2
247
+ });
248
+ } catch (err) {
249
+ lastErr = err;
250
+ }
251
+ }
252
+ try {
253
+ return await decryptWithAAD(blob, normalized, void 0);
254
+ } catch {
255
+ throw lastErr || new Error("Decryption failed with provided secret.");
256
+ }
257
+ }
258
+ try {
259
+ const salt = meta?.userId || "ziphro-mcp-user";
260
+ const keys = await deriveAccountKeys(normalized, salt);
261
+ return await decryptWithAAD(blob, keys.encKey, meta);
262
+ } catch {
263
+ return legacyDecrypt(blob, normalized, meta);
264
+ }
265
+ }
266
+
267
+ // src/index.ts
268
+ import crypto, { createDecipheriv, hkdfSync } from "crypto";
269
+ import http from "http";
270
+ import { exec } from "child_process";
271
+ import { homedir, hostname, userInfo } from "os";
272
+ import fs from "fs";
273
+ import path from "path";
274
+ import { fileURLToPath } from "url";
275
+ var __filename2 = fileURLToPath(import.meta.url);
276
+ var __dirname2 = path.dirname(__filename2);
277
+ var ZIPHRO_DIR = path.join(homedir(), ".ziphro");
278
+ var MCP_CONFIG_FILE = path.join(ZIPHRO_DIR, "mcp.json");
279
+ var CLI_CREDENTIALS_FILE = path.join(ZIPHRO_DIR, "credentials");
280
+ var SEED_FILE = path.join(ZIPHRO_DIR, ".seed");
281
+ function loadEnv() {
282
+ const envPaths = [
283
+ path.join(process.cwd(), ".env"),
284
+ path.join(__dirname2, ".env"),
285
+ path.join(__dirname2, "..", ".env")
286
+ ];
287
+ for (const envPath of envPaths) {
288
+ if (fs.existsSync(envPath)) {
289
+ try {
290
+ const content = fs.readFileSync(envPath, "utf8");
291
+ for (const line of content.split(/\r?\n/)) {
292
+ const trimmed = line.trim();
293
+ if (!trimmed || trimmed.startsWith("#")) continue;
294
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
295
+ if (match) {
296
+ const key = match[1].trim();
297
+ let val = match[2].trim();
298
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
299
+ val = val.substring(1, val.length - 1);
300
+ }
301
+ if (!process.env[key]) {
302
+ process.env[key] = val;
303
+ }
304
+ }
305
+ }
306
+ break;
307
+ } catch {
308
+ }
309
+ }
310
+ }
311
+ }
312
+ loadEnv();
313
+ function getMachineKey() {
314
+ try {
315
+ if (!fs.existsSync(SEED_FILE)) return null;
316
+ const seed = fs.readFileSync(SEED_FILE);
317
+ if (seed.length < 32) return null;
318
+ const user = userInfo?.()?.username || "user";
319
+ const host = hostname() || "local";
320
+ const info = `ziphro-vault-v2-${user}-${host}-${process.platform}`;
321
+ return Buffer.from(hkdfSync("sha256", seed, Buffer.alloc(0), Buffer.from(info), 32));
322
+ } catch {
323
+ return null;
324
+ }
325
+ }
326
+ function decryptSecret(value) {
327
+ if (!value) return void 0;
328
+ if (!value.startsWith("enc:v1:")) return value;
329
+ try {
330
+ const parts = value.split(":");
331
+ if (parts.length !== 5) return void 0;
332
+ const [, , ivHex, cipherHex, tagHex] = parts;
333
+ const key = getMachineKey();
334
+ if (!key) return void 0;
335
+ const iv = Buffer.from(ivHex, "hex");
336
+ const ciphertext = Buffer.from(cipherHex, "hex");
337
+ const authTag = Buffer.from(tagHex, "hex");
338
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
339
+ decipher.setAuthTag(authTag);
340
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
341
+ return decrypted.toString("utf8");
342
+ } catch {
343
+ return void 0;
344
+ }
345
+ }
346
+ function readCliCredentials() {
347
+ try {
348
+ if (!fs.existsSync(CLI_CREDENTIALS_FILE)) return null;
349
+ const content = fs.readFileSync(CLI_CREDENTIALS_FILE, "utf8");
350
+ const ini = {};
351
+ for (const line of content.split(/\r?\n/)) {
352
+ const trimmed = line.trim();
353
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("[") || trimmed.startsWith(";")) continue;
354
+ const eq = trimmed.indexOf("=");
355
+ if (eq === -1) continue;
356
+ ini[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
357
+ }
358
+ const accessToken = decryptSecret(ini.access_token);
359
+ const vaultKey = decryptSecret(ini.vault_key);
360
+ const apiUrl = ini.api_url;
361
+ if (accessToken && vaultKey) {
362
+ return {
363
+ apiKey: accessToken,
364
+ apiSecret: vaultKey,
365
+ apiUrl
366
+ };
367
+ }
368
+ } catch {
369
+ }
370
+ return null;
371
+ }
372
+ function readMcpConfigFile() {
373
+ try {
374
+ if (!fs.existsSync(MCP_CONFIG_FILE)) return null;
375
+ const data = JSON.parse(fs.readFileSync(MCP_CONFIG_FILE, "utf8"));
376
+ return {
377
+ apiKey: data.api_key || data.apiKey || data.token,
378
+ apiSecret: data.api_secret || data.apiSecret || data.secret,
379
+ apiUrl: data.api_url || data.apiUrl || data.api
380
+ };
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+ function writeMcpConfigFile(data) {
386
+ try {
387
+ if (!fs.existsSync(ZIPHRO_DIR)) {
388
+ fs.mkdirSync(ZIPHRO_DIR, { recursive: true, mode: 448 });
389
+ }
390
+ fs.writeFileSync(MCP_CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 384 });
391
+ } catch (err) {
392
+ console.error("Failed to write mcp config file:", err);
393
+ }
394
+ }
395
+ async function runBrowserAuth(apiUrl) {
396
+ const DEFAULT_PORT = 3456;
397
+ console.error("\n" + "=".repeat(68));
398
+ console.error(" \u2726 Ziphro MCP \u2014 Trusted AI Agent Authorization");
399
+ console.error("=".repeat(68));
400
+ console.error("Starting local handshake receiver...\n");
401
+ const server = http.createServer((req, res) => {
402
+ res.setHeader("Access-Control-Allow-Origin", "*");
403
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
404
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
405
+ if (req.method === "OPTIONS") {
406
+ res.writeHead(204);
407
+ res.end();
408
+ return;
409
+ }
410
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
411
+ if (parsedUrl.pathname === "/callback") {
412
+ const handlePayload = (apiKey, apiSecret, resolvedApiUrl) => {
413
+ if (!apiKey || !apiSecret) {
414
+ res.writeHead(400, { "Content-Type": "text/html" });
415
+ res.end(`
416
+ <html><body style="font-family:sans-serif;background:#0d1117;color:#ff6b6b;padding:40px;text-align:center;">
417
+ <h2>Authorization Failed</h2>
418
+ <p>Missing api_key or api_secret parameters.</p>
419
+ </body></html>
420
+ `);
421
+ return;
422
+ }
423
+ const finalApiUrl = (resolvedApiUrl || apiUrl).replace(/\/$/, "");
424
+ writeMcpConfigFile({
425
+ api_key: apiKey,
426
+ api_secret: apiSecret,
427
+ api_url: finalApiUrl
428
+ });
429
+ res.writeHead(200, { "Content-Type": "text/html" });
430
+ res.end(`
431
+ <!DOCTYPE html>
432
+ <html>
433
+ <head>
434
+ <meta charset="utf-8" />
435
+ <title>Ziphro MCP \u2014 Authorized</title>
436
+ <style>
437
+ body {
438
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
439
+ background: #090a0f;
440
+ color: #f1f5f9;
441
+ display: flex;
442
+ align-items: center;
443
+ justify-content: center;
444
+ height: 100vh;
445
+ margin: 0;
446
+ }
447
+ .box {
448
+ background: #11141c;
449
+ border: 1px solid #1f2536;
450
+ padding: 40px;
451
+ border-radius: 14px;
452
+ text-align: center;
453
+ max-width: 440px;
454
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
455
+ }
456
+ .badge {
457
+ display: inline-block;
458
+ background: rgba(0, 223, 216, 0.1);
459
+ color: #00dfd8;
460
+ border: 1px solid rgba(0, 223, 216, 0.25);
461
+ border-radius: 999px;
462
+ padding: 4px 14px;
463
+ font-size: 11px;
464
+ font-weight: 700;
465
+ letter-spacing: 0.08em;
466
+ text-transform: uppercase;
467
+ margin-bottom: 16px;
468
+ }
469
+ h1 { font-size: 20px; font-weight: 700; margin: 0 0 10px 0; color: #ffffff; }
470
+ p { color: #94a3b8; font-size: 13px; line-height: 1.6; margin: 0 0 24px 0; }
471
+ .btn {
472
+ background: #00dfd8;
473
+ color: #090a0f;
474
+ border: none;
475
+ padding: 10px 24px;
476
+ border-radius: 8px;
477
+ font-weight: 600;
478
+ font-size: 13px;
479
+ cursor: pointer;
480
+ }
481
+ </style>
482
+ </head>
483
+ <body>
484
+ <div class="box">
485
+ <div class="badge">E2E Zero-Knowledge Connected</div>
486
+ <h1>Trusted AI Agent Authorized</h1>
487
+ <p>Ziphro MCP has received your zero-knowledge API Key and Vault Secret. You can now close this browser tab and return to your terminal.</p>
488
+ <button class="btn" onclick="window.close()">Close Window</button>
489
+ </div>
490
+ </body>
491
+ </html>
492
+ `);
493
+ console.error("\n" + "\u2714".repeat(3) + " Successfully authorized AI agent!");
494
+ console.error(`\u2714 Credentials saved securely to: ${MCP_CONFIG_FILE}`);
495
+ console.error(`\u2714 API Key: ${apiKey.slice(0, 16)}...`);
496
+ console.error("\u2714 You can now run Ziphro MCP in Claude Desktop, Cursor, or CLI without passwords!\n");
497
+ setTimeout(() => {
498
+ server.close();
499
+ process.exit(0);
500
+ }, 800);
501
+ };
502
+ if (req.method === "POST") {
503
+ let body = "";
504
+ req.on("data", (chunk) => {
505
+ body += chunk;
506
+ });
507
+ req.on("end", () => {
508
+ try {
509
+ const json = JSON.parse(body);
510
+ handlePayload(json.api_key || json.apiKey, json.api_secret || json.apiSecret, json.api_url || json.apiUrl);
511
+ } catch {
512
+ res.writeHead(400);
513
+ res.end();
514
+ }
515
+ });
516
+ } else {
517
+ const apiKey = parsedUrl.searchParams.get("api_key") || parsedUrl.searchParams.get("apiKey") || "";
518
+ const apiSecret = parsedUrl.searchParams.get("api_secret") || parsedUrl.searchParams.get("apiSecret") || "";
519
+ const resApiUrl = parsedUrl.searchParams.get("api_url") || parsedUrl.searchParams.get("apiUrl") || "";
520
+ handlePayload(apiKey, apiSecret, resApiUrl);
521
+ }
522
+ return;
523
+ }
524
+ res.writeHead(404);
525
+ res.end();
526
+ });
527
+ server.on("error", (err) => {
528
+ if (err.code === "EADDRINUSE") {
529
+ server.listen(0, "127.0.0.1");
530
+ } else {
531
+ console.error("Handshake server error:", err);
532
+ process.exit(1);
533
+ }
534
+ });
535
+ server.listen(DEFAULT_PORT, "127.0.0.1", () => {
536
+ const addr = server.address();
537
+ const port = addr.port;
538
+ const authUrl = `${apiUrl.replace(/\/$/, "")}/mcp/authorize?port=${port}&client=Ziphro%20MCP`;
539
+ console.error(`1. Opening your browser to authorize your AI agent:`);
540
+ console.error(` \u{1F449} ${authUrl}
541
+ `);
542
+ console.error(`2. If your browser did not open automatically, copy and open the link above.`);
543
+ console.error(`3. Waiting for browser authorization... (Press Ctrl+C to cancel)
544
+ `);
545
+ const startCmd = process.platform === "win32" ? `start "" "${authUrl}"` : process.platform === "darwin" ? `open "${authUrl}"` : `xdg-open "${authUrl}"`;
546
+ exec(startCmd, () => {
547
+ });
548
+ });
549
+ setTimeout(() => {
550
+ console.error("\n\u2716 Authorization timed out after 5 minutes.");
551
+ server.close();
552
+ process.exit(1);
553
+ }, 5 * 60 * 1e3);
554
+ }
555
+ var cliApiKey = "";
556
+ var cliApiSecret = "";
557
+ var cliApiUrl = "";
558
+ var isAuthCommand = false;
559
+ for (let i = 2; i < process.argv.length; i++) {
560
+ const arg = process.argv[i];
561
+ if (arg === "auth" || arg === "--auth" || arg === "login" || arg === "--login") {
562
+ isAuthCommand = true;
563
+ } else if ((arg === "--api-key" || arg === "--token" || arg === "-t") && process.argv[i + 1]) {
564
+ cliApiKey = process.argv[i + 1];
565
+ i++;
566
+ } else if ((arg === "--api-secret" || arg === "--secret" || arg === "-s" || arg === "--password") && process.argv[i + 1]) {
567
+ cliApiSecret = process.argv[i + 1];
568
+ i++;
569
+ } else if ((arg === "--api" || arg === "--api-url" || arg === "-a") && process.argv[i + 1]) {
570
+ cliApiUrl = process.argv[i + 1];
571
+ i++;
572
+ }
573
+ }
574
+ var DEFAULT_API_URL = "https://api-ziphro.vercel.app";
575
+ var ZIPHRO_API = (cliApiUrl || process.env.ZIPHRO_API || process.env.ZIPHRO_API_URL || readMcpConfigFile()?.apiUrl || readCliCredentials()?.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
576
+ if (isAuthCommand) {
577
+ runBrowserAuth(ZIPHRO_API);
578
+ } else {
579
+ startMcpServer();
580
+ }
581
+ async function startMcpServer() {
582
+ const mcpConfigFile = readMcpConfigFile();
583
+ const cliCreds = readCliCredentials();
584
+ const ZIPHRO_API_KEY = cliApiKey || process.env.ZIPHRO_API_KEY || process.env.ZIPHRO_TOKEN || mcpConfigFile?.apiKey || cliCreds?.apiKey || "";
585
+ const ZIPHRO_API_SECRET = cliApiSecret || process.env.ZIPHRO_API_SECRET || process.env.ZIPHRO_PASSWORD || mcpConfigFile?.apiSecret || cliCreds?.apiSecret || "";
586
+ if (!ZIPHRO_API_KEY || !ZIPHRO_API_SECRET) {
587
+ console.error("\n" + "=".repeat(68));
588
+ console.error(" \u2726 Ziphro MCP Server \u2014 Authentication Required");
589
+ console.error("=".repeat(68));
590
+ console.error("No credentials found. Ziphro MCP uses zero-knowledge API Key & Secret");
591
+ console.error("pairing. Your master password is NEVER asked for or stored.\n");
592
+ console.error("To authorize your AI agent in one step, run:");
593
+ console.error(" \u{1F449} npx ziphro-mcp auth\n");
594
+ console.error("Or set environment variables in your client config:");
595
+ console.error(' ZIPHRO_API_KEY="zp_live_..."');
596
+ console.error(' ZIPHRO_API_SECRET="zs_live_..."');
597
+ console.error(` ZIPHRO_API="${ZIPHRO_API}"`);
598
+ console.error("=".repeat(68) + "\n");
599
+ process.exit(1);
600
+ }
601
+ let currentUserId = "mcp";
602
+ let currentRevision = 1;
603
+ function resolveUniqueId(tasks, id) {
604
+ const matches = tasks.filter((t) => t.id === id || t.id.startsWith(id));
605
+ if (matches.length === 0) {
606
+ return { errorText: `Task with ID matching "${id}" not found.` };
607
+ }
608
+ if (matches.length > 1) {
609
+ const list = matches.map((t) => `- \`${t.id.substring(0, 8)}\`: ${t.description}`).join("\n");
610
+ return { errorText: `"${id}" matches ${matches.length} tasks \u2014 use a longer/exact ID:
611
+ ${list}` };
612
+ }
613
+ return { targetId: matches[0].id };
614
+ }
615
+ async function fetchTasks() {
616
+ try {
617
+ const res = await fetch(`${ZIPHRO_API}/api/v1/tasks`, {
618
+ method: "GET",
619
+ headers: {
620
+ Authorization: `Bearer ${ZIPHRO_API_KEY}`,
621
+ "Content-Type": "application/json"
622
+ }
623
+ });
624
+ if (res.status === 404) {
625
+ return { tasks: [], version: "" };
626
+ }
627
+ if (!res.ok) {
628
+ const text = await res.text();
629
+ throw new Error(`Sync download failed: ${res.status} ${text}`);
630
+ }
631
+ const json = await res.json();
632
+ if (!json.success || !json.data || !json.data.encrypted_blob) {
633
+ return { tasks: [], version: "" };
634
+ }
635
+ if (json.data.userId || json.data.user_id) {
636
+ currentUserId = json.data.userId || json.data.user_id;
637
+ }
638
+ if (json.data.revision) {
639
+ currentRevision = json.data.revision;
640
+ }
641
+ const decrypted = await decrypt(json.data.encrypted_blob, ZIPHRO_API_SECRET, {
642
+ userId: currentUserId,
643
+ revision: currentRevision,
644
+ version: 2
645
+ });
646
+ const bundle = JSON.parse(decrypted);
647
+ return { tasks: bundle.tasks || [], version: json.data.checksum || "" };
648
+ } catch (err) {
649
+ console.error("Error fetching tasks from cloud:", err);
650
+ throw new McpError(ErrorCode.InternalError, `Could not retrieve task list: ${err.message}`);
651
+ }
652
+ }
653
+ async function uploadTasks(tasks, expectedVersion) {
654
+ try {
655
+ currentRevision = currentRevision + 1;
656
+ const bundle = {
657
+ tasks,
658
+ schema_version: "2",
659
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
660
+ };
661
+ const plaintext = JSON.stringify(bundle);
662
+ const blobChecksum = checksum(plaintext);
663
+ const blob = await encrypt(plaintext, ZIPHRO_API_SECRET, {
664
+ userId: currentUserId || "mcp",
665
+ revision: currentRevision,
666
+ version: 2
667
+ });
668
+ const res = await fetch(`${ZIPHRO_API}/api/v1/tasks`, {
669
+ method: "POST",
670
+ headers: {
671
+ Authorization: `Bearer ${ZIPHRO_API_KEY}`,
672
+ "Content-Type": "application/json"
673
+ },
674
+ body: JSON.stringify({
675
+ encrypted_blob: blob,
676
+ checksum: blobChecksum,
677
+ expected_checksum: expectedVersion,
678
+ device_id: "mcp"
679
+ })
680
+ });
681
+ if (res.status === 409) {
682
+ throw new McpError(ErrorCode.InternalError, "Task list changed on another device \u2014 please retry.");
683
+ }
684
+ if (!res.ok) {
685
+ const text = await res.text();
686
+ throw new Error(`Sync upload failed: ${res.status} ${text}`);
687
+ }
688
+ } catch (err) {
689
+ console.error("Error uploading tasks to cloud:", err);
690
+ throw new McpError(ErrorCode.InternalError, `Could not sync changes to cloud: ${err.message}`);
691
+ }
692
+ }
693
+ const server = new Server(
694
+ {
695
+ name: "ziphro-mcp",
696
+ version: "2.0.0-preview.0"
697
+ },
698
+ {
699
+ capabilities: {
700
+ tools: {}
701
+ }
702
+ }
703
+ );
704
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
705
+ return {
706
+ tools: [
707
+ {
708
+ name: "list_tasks",
709
+ description: "List user tasks with optional status and priority filters. Returns decrypted task details.",
710
+ inputSchema: {
711
+ type: "object",
712
+ properties: {
713
+ status: {
714
+ type: "string",
715
+ enum: ["pending", "in_progress", "completed", "all"],
716
+ description: "Filter tasks by status (default: all)"
717
+ },
718
+ priority: {
719
+ type: "string",
720
+ enum: ["high", "medium", "low", "all"],
721
+ description: "Filter tasks by priority level (default: all)"
722
+ },
723
+ tag: {
724
+ type: "string",
725
+ description: "Filter tasks by a specific tag"
726
+ }
727
+ }
728
+ }
729
+ },
730
+ {
731
+ name: "add_task",
732
+ description: "Create a new task with description, priority, due date, and tags.",
733
+ inputSchema: {
734
+ type: "object",
735
+ required: ["description"],
736
+ properties: {
737
+ description: {
738
+ type: "string",
739
+ description: "What needs to be done"
740
+ },
741
+ priority: {
742
+ type: "string",
743
+ enum: ["high", "medium", "low"],
744
+ description: "Task priority level (default: medium)"
745
+ },
746
+ due_date: {
747
+ type: "string",
748
+ description: "ISO 8601 due date string (optional)"
749
+ },
750
+ tags: {
751
+ type: "array",
752
+ items: { type: "string" },
753
+ description: "List of tags/labels for categorizing the task"
754
+ }
755
+ }
756
+ }
757
+ },
758
+ {
759
+ name: "complete_task",
760
+ description: "Mark an active task as completed with an optional note.",
761
+ inputSchema: {
762
+ type: "object",
763
+ required: ["task_id"],
764
+ properties: {
765
+ task_id: {
766
+ type: "string",
767
+ description: "The UUID or short prefix ID of the task to complete"
768
+ },
769
+ note: {
770
+ type: "string",
771
+ description: "Optional completion note"
772
+ }
773
+ }
774
+ }
775
+ },
776
+ {
777
+ name: "get_task_stats",
778
+ description: "Get aggregated metrics and status breakdown of the task list.",
779
+ inputSchema: {
780
+ type: "object",
781
+ properties: {}
782
+ }
783
+ },
784
+ {
785
+ name: "suggest_priorities",
786
+ description: "Rank and suggest high-priority or overdue tasks to focus on next.",
787
+ inputSchema: {
788
+ type: "object",
789
+ properties: {
790
+ limit: {
791
+ type: "number",
792
+ description: "Maximum number of suggestions to return (default: 5)"
793
+ }
794
+ }
795
+ }
796
+ },
797
+ {
798
+ name: "search_tasks",
799
+ description: "Full-text search tasks by keyword, tag, or due date range.",
800
+ inputSchema: {
801
+ type: "object",
802
+ properties: {
803
+ query: { type: "string", description: "Search keyword matching task description" },
804
+ tags: { type: "array", items: { type: "string" }, description: "List of tags to filter by" },
805
+ due_before: { type: "string", description: "ISO due date string" }
806
+ }
807
+ }
808
+ },
809
+ {
810
+ name: "update_task",
811
+ description: "Update properties (description, priority, status, tags, due date) of an existing task.",
812
+ inputSchema: {
813
+ type: "object",
814
+ required: ["id"],
815
+ properties: {
816
+ id: { type: "string", description: "UUID or unique prefix ID of the task to update" },
817
+ description: { type: "string", description: "New description" },
818
+ priority: { type: "string", enum: ["high", "medium", "low"], description: "New priority level" },
819
+ status: { type: "string", enum: ["pending", "in_progress", "completed"], description: "New status" },
820
+ due_date: { type: "string", description: "New ISO due date string" },
821
+ due: { type: "string", description: "Alias for due_date" },
822
+ tags: { type: "array", items: { type: "string" }, description: "New set of tags" }
823
+ }
824
+ }
825
+ },
826
+ {
827
+ name: "bulk_complete",
828
+ description: "Mark multiple tasks as completed in a single action.",
829
+ inputSchema: {
830
+ type: "object",
831
+ required: ["ids"],
832
+ properties: {
833
+ ids: { type: "array", items: { type: "string" }, description: "List of task UUIDs" },
834
+ note: { type: "string", description: "Optional completion note for all tasks" }
835
+ }
836
+ }
837
+ },
838
+ {
839
+ name: "get_overdue",
840
+ description: "Retrieve all pending or in-progress tasks that are past their due dates.",
841
+ inputSchema: {
842
+ type: "object",
843
+ properties: {}
844
+ }
845
+ }
846
+ ]
847
+ };
848
+ });
849
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
850
+ const { name, arguments: args } = request.params;
851
+ try {
852
+ switch (name) {
853
+ case "list_tasks": {
854
+ const filterStatus = args?.status || "all";
855
+ const filterPriority = args?.priority || "all";
856
+ const filterTag = args?.tag || "";
857
+ const { tasks } = await fetchTasks();
858
+ let filtered = tasks;
859
+ if (filterStatus !== "all") {
860
+ filtered = filtered.filter((t) => t.status === filterStatus);
861
+ }
862
+ if (filterPriority !== "all") {
863
+ filtered = filtered.filter((t) => t.priority === filterPriority);
864
+ }
865
+ if (filterTag) {
866
+ filtered = filtered.filter((t) => t.tags.includes(filterTag));
867
+ }
868
+ if (filtered.length === 0) {
869
+ return {
870
+ content: [{ type: "text", text: "No matching tasks found." }]
871
+ };
872
+ }
873
+ const tableHeader = "| Status | Priority | Description | Due Date | Tags | Short ID |\n| :---: | :---: | :--- | :--- | :--- | :---: |";
874
+ const rows = filtered.map((t) => {
875
+ const statusEmoji = t.status === "completed" ? "\u2705" : t.status === "in_progress" ? "\u26A1" : "\u23F3";
876
+ const priorityEmoji = t.priority === "high" ? "\u{1F534} High" : t.priority === "medium" ? "\u{1F7E1} Medium" : "\u{1F7E2} Low";
877
+ const due = t.due_date ? new Date(t.due_date).toLocaleDateString() : "-";
878
+ const tagsStr = t.tags.length > 0 ? t.tags.map((tag) => `\`${tag}\``).join(", ") : "-";
879
+ const shortId = `\`${t.id.substring(0, 8)}\``;
880
+ return `| ${statusEmoji} | ${priorityEmoji} | ${t.description} | ${due} | ${tagsStr} | ${shortId} |`;
881
+ }).join("\n");
882
+ const formatted = `${tableHeader}
883
+ ${rows}`;
884
+ return {
885
+ content: [{ type: "text", text: `### \u{1FA90} Found ${filtered.length} task${filtered.length === 1 ? "" : "s"}:
886
+
887
+ ${formatted}` }]
888
+ };
889
+ }
890
+ case "add_task": {
891
+ const description = args?.description;
892
+ const priority = args?.priority || "medium";
893
+ const due_date = args?.due_date;
894
+ const tags = args?.tags || [];
895
+ if (!description) {
896
+ throw new McpError(ErrorCode.InvalidParams, "Description is required.");
897
+ }
898
+ const { tasks, version } = await fetchTasks();
899
+ const newTask = {
900
+ id: crypto.randomUUID(),
901
+ description,
902
+ priority,
903
+ status: "pending",
904
+ tags,
905
+ due_date,
906
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
907
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
908
+ device_id: "mcp",
909
+ vector_clock: { mcp: 1 }
910
+ };
911
+ const nextTasks = [...tasks, newTask];
912
+ await uploadTasks(nextTasks, version);
913
+ return {
914
+ content: [{ type: "text", text: `Successfully created task: "${newTask.description}" with ID: ${newTask.id}` }]
915
+ };
916
+ }
917
+ case "complete_task": {
918
+ const taskId = args?.task_id;
919
+ const note = args?.note;
920
+ if (!taskId) {
921
+ throw new McpError(ErrorCode.InvalidParams, "Task ID is required.");
922
+ }
923
+ const { tasks, version } = await fetchTasks();
924
+ const resolution = resolveUniqueId(tasks, taskId);
925
+ if (resolution.errorText) {
926
+ return {
927
+ content: [{ type: "text", text: resolution.errorText }],
928
+ isError: true
929
+ };
930
+ }
931
+ const targetId = resolution.targetId;
932
+ const nextTasks = tasks.map((t) => {
933
+ if (t.id === targetId) {
934
+ return {
935
+ ...t,
936
+ status: "completed",
937
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
938
+ completion_note: note,
939
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
940
+ };
941
+ }
942
+ return t;
943
+ });
944
+ await uploadTasks(nextTasks, version);
945
+ return {
946
+ content: [{ type: "text", text: `Task completed successfully.` }]
947
+ };
948
+ }
949
+ case "get_task_stats": {
950
+ const { tasks } = await fetchTasks();
951
+ const total = tasks.length;
952
+ const completed = tasks.filter((t) => t.status === "completed").length;
953
+ const pending = tasks.filter((t) => t.status === "pending").length;
954
+ const inProgress = tasks.filter((t) => t.status === "in_progress").length;
955
+ const high = tasks.filter((t) => t.priority === "high").length;
956
+ const medium = tasks.filter((t) => t.priority === "medium").length;
957
+ const low = tasks.filter((t) => t.priority === "low").length;
958
+ const overdue = tasks.filter((t) => t.due_date && t.status !== "completed" && new Date(t.due_date) < /* @__PURE__ */ new Date()).length;
959
+ const completionRate = total > 0 ? Math.round(completed / total * 100) : 0;
960
+ const responseText = [
961
+ `\u2726 Ziphro Dashboard Metrics \u2726`,
962
+ `------------------------------`,
963
+ `Total Tasks: ${total}`,
964
+ `Completed: ${completed} (${completionRate}%)`,
965
+ `In Progress: ${inProgress}`,
966
+ `Backlog/Pending: ${pending}`,
967
+ `Overdue: ${overdue}`,
968
+ `------------------------------`,
969
+ `Priority Breakdown:`,
970
+ ` - High: ${high}`,
971
+ ` - Medium: ${medium}`,
972
+ ` - Low: ${low}`
973
+ ].join("\n");
974
+ return {
975
+ content: [{ type: "text", text: responseText }]
976
+ };
977
+ }
978
+ case "suggest_priorities": {
979
+ const limit = args?.limit || 5;
980
+ const { tasks } = await fetchTasks();
981
+ const activeTasks = tasks.filter((t) => t.status !== "completed");
982
+ const sorted = activeTasks.sort((a, b) => {
983
+ const aOverdue = a.due_date && new Date(a.due_date) < /* @__PURE__ */ new Date();
984
+ const bOverdue = b.due_date && new Date(b.due_date) < /* @__PURE__ */ new Date();
985
+ if (aOverdue && !bOverdue) return -1;
986
+ if (!aOverdue && bOverdue) return 1;
987
+ const priorityWeights = { high: 3, medium: 2, low: 1 };
988
+ const aWeight = priorityWeights[a.priority];
989
+ const bWeight = priorityWeights[b.priority];
990
+ if (aWeight !== bWeight) {
991
+ return bWeight - aWeight;
992
+ }
993
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
994
+ });
995
+ const suggestions = sorted.slice(0, limit);
996
+ if (suggestions.length === 0) {
997
+ return {
998
+ content: [{ type: "text", text: "You have no active pending tasks!" }]
999
+ };
1000
+ }
1001
+ const formatted = suggestions.map((t, idx) => {
1002
+ const priorityEmoji = t.priority === "high" ? "\u{1F534} High" : t.priority === "medium" ? "\u{1F7E1} Medium" : "\u{1F7E2} Low";
1003
+ const due = t.due_date ? ` \u{1F4C5} *Due: ${new Date(t.due_date).toLocaleDateString()}*` : "";
1004
+ const overdueMarker = t.due_date && new Date(t.due_date) < /* @__PURE__ */ new Date() ? " \u26A0\uFE0F **[OVERDUE]**" : "";
1005
+ const shortId = `\`${t.id.substring(0, 8)}\``;
1006
+ return `${idx + 1}. **[${priorityEmoji}]** ${t.description}${due}${overdueMarker} *(ID: ${shortId})*`;
1007
+ }).join("\n");
1008
+ return {
1009
+ content: [{ type: "text", text: `### \u{1F680} Suggested tasks to work on next:
1010
+
1011
+ ${formatted}` }]
1012
+ };
1013
+ }
1014
+ case "search_tasks": {
1015
+ const query = (args?.query || "").toLowerCase();
1016
+ const filterTags = args?.tags || [];
1017
+ const dueBefore = args?.due_before || "";
1018
+ const { tasks } = await fetchTasks();
1019
+ let filtered = tasks;
1020
+ if (query) {
1021
+ filtered = filtered.filter((t) => t.description.toLowerCase().includes(query));
1022
+ }
1023
+ if (filterTags.length > 0) {
1024
+ filtered = filtered.filter((t) => filterTags.every((tag) => t.tags.includes(tag)));
1025
+ }
1026
+ if (dueBefore) {
1027
+ const limitDate = new Date(dueBefore);
1028
+ filtered = filtered.filter((t) => t.due_date && new Date(t.due_date) <= limitDate);
1029
+ }
1030
+ if (filtered.length === 0) {
1031
+ return {
1032
+ content: [{ type: "text", text: "No matching tasks found." }]
1033
+ };
1034
+ }
1035
+ const tableHeader = "| Status | Priority | Description | Due Date | Tags | Short ID |\n| :---: | :---: | :--- | :--- | :--- | :---: |";
1036
+ const rows = filtered.map((t) => {
1037
+ const statusEmoji = t.status === "completed" ? "\u2705" : t.status === "in_progress" ? "\u26A1" : "\u23F3";
1038
+ const priorityEmoji = t.priority === "high" ? "\u{1F534} High" : t.priority === "medium" ? "\u{1F7E1} Medium" : "\u{1F7E2} Low";
1039
+ const due = t.due_date ? new Date(t.due_date).toLocaleDateString() : "-";
1040
+ const tagsStr = t.tags.length > 0 ? t.tags.map((tag) => `\`${tag}\``).join(", ") : "-";
1041
+ const shortId = `\`${t.id.substring(0, 8)}\``;
1042
+ return `| ${statusEmoji} | ${priorityEmoji} | ${t.description} | ${due} | ${tagsStr} | ${shortId} |`;
1043
+ }).join("\n");
1044
+ return {
1045
+ content: [{ type: "text", text: `### \u{1F50D} Search results (${filtered.length} matches):
1046
+
1047
+ ${tableHeader}
1048
+ ${rows}` }]
1049
+ };
1050
+ }
1051
+ case "update_task": {
1052
+ const id = args?.id;
1053
+ const description = args?.description;
1054
+ const priority = args?.priority;
1055
+ const status = args?.status;
1056
+ const due_date = args?.due_date || args?.due;
1057
+ const tags = args?.tags;
1058
+ if (!id) {
1059
+ throw new McpError(ErrorCode.InvalidParams, "Task ID (id) is required.");
1060
+ }
1061
+ const { tasks, version } = await fetchTasks();
1062
+ const resolution = resolveUniqueId(tasks, id);
1063
+ if (resolution.errorText) {
1064
+ return {
1065
+ content: [{ type: "text", text: resolution.errorText }],
1066
+ isError: true
1067
+ };
1068
+ }
1069
+ const targetId = resolution.targetId;
1070
+ const nextTasks = tasks.map((t) => {
1071
+ if (t.id === targetId) {
1072
+ const updated = { ...t };
1073
+ if (description !== void 0) updated.description = description;
1074
+ if (priority !== void 0) updated.priority = priority;
1075
+ if (status !== void 0) {
1076
+ updated.status = status;
1077
+ if (status === "completed") {
1078
+ updated.completed_at = (/* @__PURE__ */ new Date()).toISOString();
1079
+ } else {
1080
+ updated.completed_at = void 0;
1081
+ }
1082
+ }
1083
+ if (due_date !== void 0) {
1084
+ updated.due_date = due_date ? new Date(due_date).toISOString() : void 0;
1085
+ }
1086
+ if (tags !== void 0) updated.tags = tags;
1087
+ updated.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1088
+ updated.vector_clock = { ...t.vector_clock, mcp: (t.vector_clock["mcp"] || 0) + 1 };
1089
+ return updated;
1090
+ }
1091
+ return t;
1092
+ });
1093
+ await uploadTasks(nextTasks, version);
1094
+ return {
1095
+ content: [{ type: "text", text: `Task updated successfully.` }]
1096
+ };
1097
+ }
1098
+ case "bulk_complete": {
1099
+ const ids = args?.ids;
1100
+ const note = args?.note;
1101
+ if (!ids || ids.length === 0) {
1102
+ throw new McpError(ErrorCode.InvalidParams, "IDs list (ids) is required.");
1103
+ }
1104
+ const { tasks, version } = await fetchTasks();
1105
+ const targetIds = [];
1106
+ for (const id of ids) {
1107
+ const resolution = resolveUniqueId(tasks, id);
1108
+ if (resolution.errorText) {
1109
+ return {
1110
+ content: [{ type: "text", text: resolution.errorText }],
1111
+ isError: true
1112
+ };
1113
+ }
1114
+ targetIds.push(resolution.targetId);
1115
+ }
1116
+ const nextTasks = tasks.map((t) => {
1117
+ if (targetIds.includes(t.id)) {
1118
+ return {
1119
+ ...t,
1120
+ status: "completed",
1121
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
1122
+ completion_note: note || t.completion_note,
1123
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1124
+ vector_clock: { ...t.vector_clock, mcp: (t.vector_clock["mcp"] || 0) + 1 }
1125
+ };
1126
+ }
1127
+ return t;
1128
+ });
1129
+ await uploadTasks(nextTasks, version);
1130
+ return {
1131
+ content: [{ type: "text", text: `Successfully completed ${targetIds.length} tasks.` }]
1132
+ };
1133
+ }
1134
+ case "get_overdue": {
1135
+ const { tasks } = await fetchTasks();
1136
+ const now = /* @__PURE__ */ new Date();
1137
+ const overdue = tasks.filter((t) => t.due_date && t.status !== "completed" && new Date(t.due_date) < now);
1138
+ if (overdue.length === 0) {
1139
+ return {
1140
+ content: [{ type: "text", text: "Nice job! You have no overdue tasks." }]
1141
+ };
1142
+ }
1143
+ const tableHeader = "| Priority | Description | Due Date | Tags | Short ID |\n| :---: | :--- | :--- | :--- | :---: |";
1144
+ const rows = overdue.map((t) => {
1145
+ const priorityEmoji = t.priority === "high" ? "\u{1F534} High" : t.priority === "medium" ? "\u{1F7E1} Medium" : "\u{1F7E2} Low";
1146
+ const due = t.due_date ? new Date(t.due_date).toLocaleDateString() : "-";
1147
+ const tagsStr = t.tags.length > 0 ? t.tags.map((tag) => `\`${tag}\``).join(", ") : "-";
1148
+ const shortId = `\`${t.id.substring(0, 8)}\``;
1149
+ return `| ${priorityEmoji} | ${t.description} | **${due}** | ${tagsStr} | ${shortId} |`;
1150
+ }).join("\n");
1151
+ return {
1152
+ content: [{ type: "text", text: `### \u26A0\uFE0F Found ${overdue.length} overdue task${overdue.length === 1 ? "" : "s"}:
1153
+
1154
+ ${tableHeader}
1155
+ ${rows}` }]
1156
+ };
1157
+ }
1158
+ default:
1159
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1160
+ }
1161
+ } catch (err) {
1162
+ return {
1163
+ content: [{ type: "text", text: `Error executing tool "${name}": ${err.message}` }],
1164
+ isError: true
1165
+ };
1166
+ }
1167
+ });
1168
+ const transport = new StdioServerTransport();
1169
+ await server.connect(transport);
1170
+ console.error("Ziphro MCP server running on stdio");
1171
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "ziphro-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.0-preview.0",
4
+ "description": "Ziphro Model Context Protocol (MCP) server for zero-knowledge E2E encrypted task management",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "bin": {
9
+ "ziphro-mcp": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "scripts",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format esm --out-dir dist --shims --clean",
18
+ "start": "node dist/index.js",
19
+ "dev": "tsx src/index.ts",
20
+ "lint": "tsc --noEmit",
21
+ "postinstall": "node scripts/postinstall.js"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/AJAYMYTH/TaskAir.git",
29
+ "directory": "mcp-server"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/AJAYMYTH/TaskAir/issues"
33
+ },
34
+ "homepage": "https://github.com/AJAYMYTH/TaskAir#readme",
35
+ "keywords": [
36
+ "ziphro",
37
+ "mcp",
38
+ "model-context-protocol",
39
+ "e2e-encryption",
40
+ "zero-knowledge",
41
+ "task-management"
42
+ ],
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.0.1",
48
+ "@ziphro/crdt": "2.0.0-preview.0",
49
+ "@ziphro/crypto": "2.0.0-preview.0",
50
+ "@ziphro/types": "2.0.0-preview.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.14.0",
54
+ "tsup": "^8.1.0",
55
+ "tsx": "^4.15.0",
56
+ "typescript": "^5.4.5"
57
+ }
58
+ }
@@ -0,0 +1,30 @@
1
+ const ORANGE = '\x1b[38;2;245;78;0m';
2
+ const WHITE = '\x1b[38;2;245;244;239m';
3
+ const MUTED = '\x1b[38;2;142;142;147m';
4
+ const BOLD = '\x1b[1m';
5
+ const RESET = '\x1b[0m';
6
+
7
+ const ASCII_BANNER = [
8
+ { ziph: 'โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—', ro: 'โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— ' },
9
+ { ziph: 'โ•šโ•โ•โ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘', ro: 'โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—' },
10
+ { ziph: ' โ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘', ro: 'โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘' },
11
+ { ziph: ' โ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘', ro: 'โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘' },
12
+ { ziph: 'โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘', ro: 'โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•' },
13
+ { ziph: 'โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•', ro: 'โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• ' },
14
+ ];
15
+
16
+ function run() {
17
+ // Never emit stdout banners during CI or non-interactive npx stdio invocations
18
+ if (!process.stdout.isTTY || process.env.CI || process.env.npm_config_loglevel === 'silent') {
19
+ return;
20
+ }
21
+
22
+ console.log('');
23
+ for (const row of ASCII_BANNER) {
24
+ console.log(`${BOLD}${WHITE}${row.ziph}${ORANGE}${row.ro}${RESET}`);
25
+ }
26
+ console.log(`${MUTED} โœฆ Ziphro MCP Server v2.0.0-preview.0 ยท Zero-Knowledge Vault โœฆ${RESET}`);
27
+ console.log(`${MUTED} Next: run ${ORANGE}npx ziphro-mcp@preview auth${MUTED} to authorize your AI client.${RESET}\n`);
28
+ }
29
+
30
+ run();