@tobiashochguertel/taskbook-mcp-server 1.9.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/dist/index.js ADDED
@@ -0,0 +1,876 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/transport/stdio.ts
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/client/crypto.ts
8
+ var HEX_RE = /^[0-9a-fA-F]+$/;
9
+ function hexToBytes(hex) {
10
+ const bytes = new Uint8Array(hex.length / 2);
11
+ for (let i = 0;i < bytes.length; i++) {
12
+ bytes[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
13
+ }
14
+ return bytes;
15
+ }
16
+ function base64Encode(data) {
17
+ let binary = "";
18
+ for (const byte of data) {
19
+ binary += String.fromCharCode(byte);
20
+ }
21
+ return btoa(binary);
22
+ }
23
+ function base64Decode(b64) {
24
+ const binary = atob(b64);
25
+ const bytes = new Uint8Array(binary.length);
26
+ for (let i = 0;i < binary.length; i++) {
27
+ bytes[i] = binary.charCodeAt(i);
28
+ }
29
+ return bytes;
30
+ }
31
+ function parseKeyBytes(key) {
32
+ const trimmed = key.trim();
33
+ if (trimmed.length === 64 && HEX_RE.test(trimmed)) {
34
+ return hexToBytes(trimmed);
35
+ }
36
+ return base64Decode(trimmed);
37
+ }
38
+ async function deriveKey(encryptionKey) {
39
+ const keyData = parseKeyBytes(encryptionKey);
40
+ if (keyData.length !== 32) {
41
+ throw new Error(`Invalid key length: expected 32 bytes, got ${keyData.length}`);
42
+ }
43
+ return crypto.subtle.importKey("raw", keyData.buffer, "AES-GCM", false, ["encrypt", "decrypt"]);
44
+ }
45
+ async function encrypt(plaintext, key) {
46
+ const iv = crypto.getRandomValues(new Uint8Array(12));
47
+ const encoded = new TextEncoder().encode(plaintext);
48
+ const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
49
+ return {
50
+ data: base64Encode(new Uint8Array(ciphertext)),
51
+ nonce: base64Encode(iv)
52
+ };
53
+ }
54
+ async function decrypt(data, nonce, key) {
55
+ const ciphertext = base64Decode(data);
56
+ const iv = base64Decode(nonce);
57
+ const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext.buffer);
58
+ return new TextDecoder().decode(plaintext);
59
+ }
60
+
61
+ // src/client/api.ts
62
+ class TaskbookClient {
63
+ baseUrl;
64
+ token;
65
+ cryptoKey = null;
66
+ encryptionKeyRaw;
67
+ constructor(config) {
68
+ this.baseUrl = config.serverUrl.replace(/\/+$/, "");
69
+ this.token = config.token;
70
+ this.encryptionKeyRaw = config.encryptionKey;
71
+ }
72
+ async getKey() {
73
+ if (!this.cryptoKey) {
74
+ this.cryptoKey = await deriveKey(this.encryptionKeyRaw);
75
+ }
76
+ return this.cryptoKey;
77
+ }
78
+ async request(method, path, body) {
79
+ const url = `${this.baseUrl}${path}`;
80
+ const headers = {
81
+ Authorization: `Bearer ${this.token}`,
82
+ "Content-Type": "application/json"
83
+ };
84
+ const resp = await fetch(url, {
85
+ method,
86
+ headers,
87
+ body: body ? JSON.stringify(body) : undefined
88
+ });
89
+ if (!resp.ok) {
90
+ const text = await resp.text();
91
+ throw new Error(`Taskbook API ${method} ${path}: ${resp.status} ${text}`);
92
+ }
93
+ const contentType = resp.headers.get("content-type") ?? "";
94
+ if (contentType.includes("application/json")) {
95
+ return await resp.json();
96
+ }
97
+ return {};
98
+ }
99
+ async decryptItems(encrypted) {
100
+ const key = await this.getKey();
101
+ const result = {};
102
+ for (const [id, item] of Object.entries(encrypted)) {
103
+ try {
104
+ const json = await decrypt(item.data, item.nonce, key);
105
+ result[id] = JSON.parse(json);
106
+ } catch {}
107
+ }
108
+ return result;
109
+ }
110
+ async encryptItem(item) {
111
+ const key = await this.getKey();
112
+ return encrypt(JSON.stringify(item), key);
113
+ }
114
+ async health() {
115
+ return this.request("GET", "/api/v1/health");
116
+ }
117
+ async me() {
118
+ return this.request("GET", "/api/v1/me");
119
+ }
120
+ async getItems() {
121
+ const resp = await this.request("GET", "/api/v1/items");
122
+ return this.decryptItems(resp.items);
123
+ }
124
+ async getArchive() {
125
+ const resp = await this.request("GET", "/api/v1/items/archive");
126
+ return this.decryptItems(resp.items);
127
+ }
128
+ async putItems(items) {
129
+ const encrypted = {};
130
+ for (const [id, item] of Object.entries(items)) {
131
+ encrypted[id] = await this.encryptItem(item);
132
+ }
133
+ await this.request("PUT", "/api/v1/items", { items: encrypted });
134
+ }
135
+ async putArchive(items) {
136
+ const encrypted = {};
137
+ for (const [id, item] of Object.entries(items)) {
138
+ encrypted[id] = await this.encryptItem(item);
139
+ }
140
+ await this.request("PUT", "/api/v1/items/archive", { items: encrypted });
141
+ }
142
+ async listTasks(board) {
143
+ const items = await this.getItems();
144
+ const tasks = Object.values(items).filter((i) => i._isTask);
145
+ if (board) {
146
+ return tasks.filter((t) => t.boards.includes(board));
147
+ }
148
+ return tasks;
149
+ }
150
+ async listNotes(board) {
151
+ const items = await this.getItems();
152
+ const notes = Object.values(items).filter((i) => !i._isTask);
153
+ if (board) {
154
+ return notes.filter((n) => n.boards.includes(board));
155
+ }
156
+ return notes;
157
+ }
158
+ async listBoards() {
159
+ const items = await this.getItems();
160
+ const boardSet = new Set;
161
+ for (const item of Object.values(items)) {
162
+ for (const b of item.boards)
163
+ boardSet.add(b);
164
+ }
165
+ return [...boardSet].sort();
166
+ }
167
+ async createTask(description, board = "My Board", priority = 1, tags = []) {
168
+ const items = await this.getItems();
169
+ const maxId = Math.max(0, ...Object.values(items).map((i) => i._id));
170
+ const now = new Date;
171
+ const task = {
172
+ _id: maxId + 1,
173
+ _date: now.toDateString().slice(0, 10),
174
+ _timestamp: now.getTime(),
175
+ _isTask: true,
176
+ description,
177
+ isStarred: false,
178
+ isComplete: false,
179
+ inProgress: false,
180
+ priority: Math.min(3, Math.max(1, priority)),
181
+ boards: [board.replace(/^@+/, "")],
182
+ tags: tags.map((t) => t.replace(/^\++/, "").toLowerCase())
183
+ };
184
+ items[String(task._id)] = task;
185
+ await this.putItems(items);
186
+ return task;
187
+ }
188
+ async createNote(description, board = "My Board", body, tags = []) {
189
+ const items = await this.getItems();
190
+ const maxId = Math.max(0, ...Object.values(items).map((i) => i._id));
191
+ const now = new Date;
192
+ const note = {
193
+ _id: maxId + 1,
194
+ _date: now.toDateString().slice(0, 10),
195
+ _timestamp: now.getTime(),
196
+ _isTask: false,
197
+ description,
198
+ body,
199
+ isStarred: false,
200
+ boards: [board.replace(/^@+/, "")],
201
+ tags: tags.map((t) => t.replace(/^\++/, "").toLowerCase())
202
+ };
203
+ items[String(note._id)] = note;
204
+ await this.putItems(items);
205
+ return note;
206
+ }
207
+ async completeTask(taskId) {
208
+ const items = await this.getItems();
209
+ const item = items[String(taskId)];
210
+ if (!item || !item._isTask) {
211
+ throw new Error(`Task ${taskId} not found`);
212
+ }
213
+ const task = item;
214
+ task.isComplete = !task.isComplete;
215
+ task.inProgress = false;
216
+ items[String(taskId)] = task;
217
+ await this.putItems(items);
218
+ return task;
219
+ }
220
+ async beginTask(taskId) {
221
+ const items = await this.getItems();
222
+ const item = items[String(taskId)];
223
+ if (!item || !item._isTask) {
224
+ throw new Error(`Task ${taskId} not found`);
225
+ }
226
+ const task = item;
227
+ task.inProgress = !task.inProgress;
228
+ items[String(taskId)] = task;
229
+ await this.putItems(items);
230
+ return task;
231
+ }
232
+ async starItem(itemId) {
233
+ const items = await this.getItems();
234
+ const item = items[String(itemId)];
235
+ if (!item)
236
+ throw new Error(`Item ${itemId} not found`);
237
+ item.isStarred = !item.isStarred;
238
+ items[String(itemId)] = item;
239
+ await this.putItems(items);
240
+ return item;
241
+ }
242
+ async deleteItem(itemId) {
243
+ const items = await this.getItems();
244
+ if (!items[String(itemId)]) {
245
+ throw new Error(`Item ${itemId} not found`);
246
+ }
247
+ delete items[String(itemId)];
248
+ await this.putItems(items);
249
+ }
250
+ async archiveItem(itemId) {
251
+ const items = await this.getItems();
252
+ const item = items[String(itemId)];
253
+ if (!item)
254
+ throw new Error(`Item ${itemId} not found`);
255
+ delete items[String(itemId)];
256
+ const archive = await this.getArchive();
257
+ archive[String(itemId)] = item;
258
+ await this.putItems(items);
259
+ await this.putArchive(archive);
260
+ }
261
+ async moveToBoard(itemId, targetBoard) {
262
+ const items = await this.getItems();
263
+ const item = items[String(itemId)];
264
+ if (!item)
265
+ throw new Error(`Item ${itemId} not found`);
266
+ item.boards = [targetBoard.replace(/^@+/, "")];
267
+ items[String(itemId)] = item;
268
+ await this.putItems(items);
269
+ return item;
270
+ }
271
+ async editItem(itemId, description) {
272
+ const items = await this.getItems();
273
+ const item = items[String(itemId)];
274
+ if (!item)
275
+ throw new Error(`Item ${itemId} not found`);
276
+ item.description = description;
277
+ items[String(itemId)] = item;
278
+ await this.putItems(items);
279
+ return item;
280
+ }
281
+ async searchItems(query) {
282
+ const items = await this.getItems();
283
+ const lower = query.toLowerCase();
284
+ return Object.values(items).filter((item) => item.description.toLowerCase().includes(lower) || item.tags.some((t) => t.includes(lower)) || item.boards.some((b) => b.toLowerCase().includes(lower)));
285
+ }
286
+ }
287
+
288
+ // src/config.ts
289
+ import { readFileSync, existsSync } from "fs";
290
+ import { join } from "path";
291
+ import { homedir } from "os";
292
+ function loadTaskbookConfig() {
293
+ const envUrl = process.env.TB_SERVER_URL;
294
+ const envToken = process.env.TB_TOKEN;
295
+ const envKey = process.env.TB_ENCRYPTION_KEY;
296
+ if (envUrl && envToken && envKey) {
297
+ return { serverUrl: envUrl, token: envToken, encryptionKey: envKey };
298
+ }
299
+ const configPath = process.env.TB_CONFIG_PATH ?? join(homedir(), ".taskbook.json");
300
+ if (!existsSync(configPath)) {
301
+ throw new Error(`Taskbook config not found at ${configPath}. ` + "Run 'tb --login' or set TB_SERVER_URL, TB_TOKEN, TB_ENCRYPTION_KEY env vars.");
302
+ }
303
+ const raw = readFileSync(configPath, "utf-8");
304
+ const config = JSON.parse(raw);
305
+ const serverUrl = envUrl ?? config.sync?.server_url;
306
+ const token = envToken ?? config.sync?.token;
307
+ const encryptionKey = envKey ?? config.encryption_key;
308
+ if (!serverUrl)
309
+ throw new Error("Missing server URL in taskbook config");
310
+ if (!token)
311
+ throw new Error("Missing auth token in taskbook config");
312
+ if (!encryptionKey)
313
+ throw new Error("Missing encryption key in taskbook config");
314
+ return { serverUrl, token, encryptionKey };
315
+ }
316
+ function loadMcpConfig() {
317
+ const transport = process.env.TB_MCP_TRANSPORT ?? "stdio";
318
+ const port = Number.parseInt(process.env.TB_MCP_PORT ?? "3100", 10);
319
+ const host = process.env.TB_MCP_HOST ?? "127.0.0.1";
320
+ return { port, host, transport };
321
+ }
322
+
323
+ // src/server.ts
324
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
325
+
326
+ // src/tools/tasks.ts
327
+ import { z } from "zod";
328
+ function formatTask(t) {
329
+ const status = t.isComplete ? "\u2714" : t.inProgress ? "\u2026" : "\u2610";
330
+ const star = t.isStarred ? "\u2605 " : "";
331
+ const pri = t.priority > 1 ? ` [P${t.priority}]` : "";
332
+ const tags = t.tags.length > 0 ? ` +${t.tags.join(" +")}` : "";
333
+ const boards = t.boards.map((b) => `@${b}`).join(" ");
334
+ return `${status} ${t._id}. ${star}${t.description}${pri}${tags} (${boards})`;
335
+ }
336
+ function registerTaskTools(server, getClient) {
337
+ server.tool("list_tasks", "List all tasks, optionally filtered by board name", { board: z.string().optional().describe("Board name to filter by") }, async ({ board }) => {
338
+ const tasks = await getClient().listTasks(board);
339
+ if (tasks.length === 0) {
340
+ return {
341
+ content: [
342
+ {
343
+ type: "text",
344
+ text: board ? `No tasks found on board @${board}.` : "No tasks found."
345
+ }
346
+ ]
347
+ };
348
+ }
349
+ const pending = tasks.filter((t) => !t.isComplete);
350
+ const done = tasks.filter((t) => t.isComplete);
351
+ const lines = [
352
+ `**Tasks** (${pending.length} pending, ${done.length} done)`,
353
+ "",
354
+ ...pending.map(formatTask),
355
+ ...done.length > 0 ? ["", "--- Completed ---", ...done.map(formatTask)] : []
356
+ ];
357
+ return { content: [{ type: "text", text: lines.join(`
358
+ `) }] };
359
+ });
360
+ server.tool("create_task", "Create a new task on a board", {
361
+ description: z.string().describe("Task description"),
362
+ board: z.string().optional().describe("Board name (default: My Board)"),
363
+ priority: z.number().min(1).max(3).optional().describe("Priority 1-3 (1=normal, 2=medium, 3=high)"),
364
+ tags: z.array(z.string()).optional().describe("Tags to attach to the task")
365
+ }, async ({ description, board, priority, tags }) => {
366
+ const task = await getClient().createTask(description, board ?? "My Board", priority ?? 1, tags ?? []);
367
+ return {
368
+ content: [
369
+ {
370
+ type: "text",
371
+ text: `Created task #${task._id}: ${task.description} on @${task.boards[0]}`
372
+ }
373
+ ]
374
+ };
375
+ });
376
+ server.tool("complete_task", "Toggle a task's completion status", {
377
+ task_id: z.number().describe("The task ID number")
378
+ }, async ({ task_id }) => {
379
+ const task = await getClient().completeTask(task_id);
380
+ const status = task.isComplete ? "completed" : "uncompleted";
381
+ return {
382
+ content: [
383
+ {
384
+ type: "text",
385
+ text: `Task #${task._id} marked as ${status}: ${task.description}`
386
+ }
387
+ ]
388
+ };
389
+ });
390
+ server.tool("begin_task", "Toggle a task's in-progress status", {
391
+ task_id: z.number().describe("The task ID number")
392
+ }, async ({ task_id }) => {
393
+ const task = await getClient().beginTask(task_id);
394
+ const status = task.inProgress ? "in-progress" : "paused";
395
+ return {
396
+ content: [
397
+ {
398
+ type: "text",
399
+ text: `Task #${task._id} marked as ${status}: ${task.description}`
400
+ }
401
+ ]
402
+ };
403
+ });
404
+ server.tool("set_task_priority", "Set a task's priority level (1=normal, 2=medium, 3=high)", {
405
+ task_id: z.number().describe("The task ID number"),
406
+ priority: z.number().min(1).max(3).describe("Priority level 1-3")
407
+ }, async ({ task_id, priority }) => {
408
+ const client = getClient();
409
+ const items = await client.getItems();
410
+ const item = items[String(task_id)];
411
+ if (!item || !item._isTask)
412
+ throw new Error(`Task ${task_id} not found`);
413
+ item.priority = priority;
414
+ items[String(task_id)] = item;
415
+ await client.putItems(items);
416
+ return {
417
+ content: [
418
+ {
419
+ type: "text",
420
+ text: `Task #${task_id} priority set to P${priority}`
421
+ }
422
+ ]
423
+ };
424
+ });
425
+ }
426
+
427
+ // src/tools/notes.ts
428
+ import { z as z2 } from "zod";
429
+ function formatNote(n) {
430
+ const star = n.isStarred ? "\u2605 " : "";
431
+ const tags = n.tags.length > 0 ? ` +${n.tags.join(" +")}` : "";
432
+ const boards = n.boards.map((b) => `@${b}`).join(" ");
433
+ const body = n.body ? `
434
+ ${n.body}` : "";
435
+ return `\uD83D\uDCDD ${n._id}. ${star}${n.description}${tags} (${boards})${body}`;
436
+ }
437
+ function registerNoteTools(server, getClient) {
438
+ server.tool("list_notes", "List all notes, optionally filtered by board", { board: z2.string().optional().describe("Board name to filter by") }, async ({ board }) => {
439
+ const notes = await getClient().listNotes(board);
440
+ if (notes.length === 0) {
441
+ return {
442
+ content: [
443
+ {
444
+ type: "text",
445
+ text: board ? `No notes found on board @${board}.` : "No notes found."
446
+ }
447
+ ]
448
+ };
449
+ }
450
+ const lines = [
451
+ `**Notes** (${notes.length})`,
452
+ "",
453
+ ...notes.map(formatNote)
454
+ ];
455
+ return { content: [{ type: "text", text: lines.join(`
456
+ `) }] };
457
+ });
458
+ server.tool("create_note", "Create a new note on a board", {
459
+ description: z2.string().describe("Note title/description"),
460
+ board: z2.string().optional().describe("Board name (default: My Board)"),
461
+ body: z2.string().optional().describe("Note body content"),
462
+ tags: z2.array(z2.string()).optional().describe("Tags to attach to the note")
463
+ }, async ({ description, board, body, tags }) => {
464
+ const note = await getClient().createNote(description, board ?? "My Board", body, tags ?? []);
465
+ return {
466
+ content: [
467
+ {
468
+ type: "text",
469
+ text: `Created note #${note._id}: ${note.description} on @${note.boards[0]}`
470
+ }
471
+ ]
472
+ };
473
+ });
474
+ }
475
+
476
+ // src/tools/boards.ts
477
+ import { z as z3 } from "zod";
478
+ function registerBoardTools(server, getClient) {
479
+ server.tool("list_boards", "List all boards with item counts", {}, async () => {
480
+ const items = await getClient().getItems();
481
+ const boardCounts = new Map;
482
+ for (const item of Object.values(items)) {
483
+ for (const board of item.boards) {
484
+ const counts = boardCounts.get(board) ?? { tasks: 0, notes: 0 };
485
+ if (item._isTask)
486
+ counts.tasks++;
487
+ else
488
+ counts.notes++;
489
+ boardCounts.set(board, counts);
490
+ }
491
+ }
492
+ if (boardCounts.size === 0) {
493
+ return {
494
+ content: [{ type: "text", text: "No boards found." }]
495
+ };
496
+ }
497
+ const lines = [
498
+ "**Boards**",
499
+ "",
500
+ ...[...boardCounts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, counts]) => `\uD83D\uDCCB @${name} (${counts.tasks} tasks, ${counts.notes} notes)`)
501
+ ];
502
+ return { content: [{ type: "text", text: lines.join(`
503
+ `) }] };
504
+ });
505
+ server.tool("move_item", "Move a task or note to a different board", {
506
+ item_id: z3.number().describe("The item ID to move"),
507
+ target_board: z3.string().describe("The target board name")
508
+ }, async ({ item_id, target_board }) => {
509
+ const item = await getClient().moveToBoard(item_id, target_board);
510
+ return {
511
+ content: [
512
+ {
513
+ type: "text",
514
+ text: `Moved item #${item._id} to @${item.boards[0]}`
515
+ }
516
+ ]
517
+ };
518
+ });
519
+ }
520
+
521
+ // src/tools/general.ts
522
+ import { z as z4 } from "zod";
523
+ function registerGeneralTools(server, getClient) {
524
+ server.tool("search_items", "Search tasks and notes by description, tag, or board name", {
525
+ query: z4.string().describe("Search query text")
526
+ }, async ({ query }) => {
527
+ const results = await getClient().searchItems(query);
528
+ if (results.length === 0) {
529
+ return {
530
+ content: [
531
+ { type: "text", text: `No items matching "${query}".` }
532
+ ]
533
+ };
534
+ }
535
+ const lines = results.map((item) => {
536
+ const type = item._isTask ? item.isComplete ? "\u2714" : "\u2610" : "\uD83D\uDCDD";
537
+ return `${type} ${item._id}. ${item.description} (@${item.boards.join(", @")})`;
538
+ });
539
+ return {
540
+ content: [
541
+ {
542
+ type: "text",
543
+ text: [`**Search results for "${query}"** (${results.length})`, "", ...lines].join(`
544
+ `)
545
+ }
546
+ ]
547
+ };
548
+ });
549
+ server.tool("edit_item", "Edit an item's description", {
550
+ item_id: z4.number().describe("The item ID to edit"),
551
+ description: z4.string().describe("New description text")
552
+ }, async ({ item_id, description }) => {
553
+ const item = await getClient().editItem(item_id, description);
554
+ return {
555
+ content: [
556
+ {
557
+ type: "text",
558
+ text: `Updated item #${item._id}: ${item.description}`
559
+ }
560
+ ]
561
+ };
562
+ });
563
+ server.tool("delete_item", "Permanently delete a task or note", {
564
+ item_id: z4.number().describe("The item ID to delete")
565
+ }, async ({ item_id }) => {
566
+ await getClient().deleteItem(item_id);
567
+ return {
568
+ content: [{ type: "text", text: `Deleted item #${item_id}` }]
569
+ };
570
+ });
571
+ server.tool("archive_item", "Move an item to the archive", {
572
+ item_id: z4.number().describe("The item ID to archive")
573
+ }, async ({ item_id }) => {
574
+ await getClient().archiveItem(item_id);
575
+ return {
576
+ content: [{ type: "text", text: `Archived item #${item_id}` }]
577
+ };
578
+ });
579
+ server.tool("star_item", "Toggle the star/bookmark on a task or note", {
580
+ item_id: z4.number().describe("The item ID to star/unstar")
581
+ }, async ({ item_id }) => {
582
+ const item = await getClient().starItem(item_id);
583
+ const status = item.isStarred ? "starred" : "unstarred";
584
+ return {
585
+ content: [
586
+ {
587
+ type: "text",
588
+ text: `Item #${item._id} ${status}: ${item.description}`
589
+ }
590
+ ]
591
+ };
592
+ });
593
+ server.tool("get_status", "Check taskbook server health and current user info", {}, async () => {
594
+ const client = getClient();
595
+ const [health, me] = await Promise.all([client.health(), client.me()]);
596
+ const items = await client.getItems();
597
+ const tasks = Object.values(items).filter((i) => i._isTask);
598
+ const done = tasks.filter((t) => t._isTask && t.isComplete).length;
599
+ const pending = tasks.length - done;
600
+ const notes = Object.values(items).filter((i) => !i._isTask).length;
601
+ const boards = await client.listBoards();
602
+ const lines = [
603
+ `**Taskbook Status**`,
604
+ `Server: ${health.status}`,
605
+ `User: ${me.username} (${me.email})`,
606
+ `Boards: ${boards.length} (${boards.map((b) => `@${b}`).join(", ")})`,
607
+ `Tasks: ${tasks.length} total (${done} done, ${pending} pending)`,
608
+ `Notes: ${notes}`
609
+ ];
610
+ return { content: [{ type: "text", text: lines.join(`
611
+ `) }] };
612
+ });
613
+ }
614
+
615
+ // src/resources/index.ts
616
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
617
+ function registerResources(server, getClient) {
618
+ server.resource("status", "taskbook://status", { description: "Taskbook server status and user info", mimeType: "application/json" }, async (uri) => {
619
+ const client = getClient();
620
+ const [health, me] = await Promise.all([client.health(), client.me()]);
621
+ return {
622
+ contents: [
623
+ {
624
+ uri: uri.href,
625
+ text: JSON.stringify({ health, user: me }, null, 2)
626
+ }
627
+ ]
628
+ };
629
+ });
630
+ server.resource("board", new ResourceTemplate("taskbook://boards/{boardName}", {
631
+ list: async () => {
632
+ const boards = await getClient().listBoards();
633
+ return {
634
+ resources: boards.map((b) => ({
635
+ uri: `taskbook://boards/${encodeURIComponent(b)}`,
636
+ name: `@${b}`,
637
+ description: `Tasks and notes on board @${b}`
638
+ }))
639
+ };
640
+ }
641
+ }), { description: "Tasks and notes on a specific board", mimeType: "application/json" }, async (uri, { boardName }) => {
642
+ const board = decodeURIComponent(boardName);
643
+ const client = getClient();
644
+ const [tasks, notes] = await Promise.all([
645
+ client.listTasks(board),
646
+ client.listNotes(board)
647
+ ]);
648
+ return {
649
+ contents: [
650
+ {
651
+ uri: uri.href,
652
+ text: JSON.stringify({ board, tasks, notes }, null, 2)
653
+ }
654
+ ]
655
+ };
656
+ });
657
+ server.resource("all-items", "taskbook://items", { description: "All tasks and notes across all boards", mimeType: "application/json" }, async (uri) => {
658
+ const items = await getClient().getItems();
659
+ return {
660
+ contents: [
661
+ {
662
+ uri: uri.href,
663
+ text: JSON.stringify(Object.values(items), null, 2)
664
+ }
665
+ ]
666
+ };
667
+ });
668
+ server.resource("archive", "taskbook://archive", { description: "Archived tasks and notes", mimeType: "application/json" }, async (uri) => {
669
+ const archive = await getClient().getArchive();
670
+ return {
671
+ contents: [
672
+ {
673
+ uri: uri.href,
674
+ text: JSON.stringify(Object.values(archive), null, 2)
675
+ }
676
+ ]
677
+ };
678
+ });
679
+ }
680
+
681
+ // src/server.ts
682
+ var SERVER_NAME = "taskbook-mcp";
683
+ var SERVER_VERSION = "1.0.0";
684
+ function createMcpServer(getClient) {
685
+ const server = new McpServer({
686
+ name: SERVER_NAME,
687
+ version: SERVER_VERSION
688
+ }, {
689
+ capabilities: {
690
+ logging: {}
691
+ }
692
+ });
693
+ registerTaskTools(server, getClient);
694
+ registerNoteTools(server, getClient);
695
+ registerBoardTools(server, getClient);
696
+ registerGeneralTools(server, getClient);
697
+ registerResources(server, getClient);
698
+ return server;
699
+ }
700
+
701
+ // src/transport/stdio.ts
702
+ async function startStdioTransport() {
703
+ const config = loadTaskbookConfig();
704
+ const client = new TaskbookClient(config);
705
+ const server = createMcpServer(() => client);
706
+ const transport = new StdioServerTransport;
707
+ await server.connect(transport);
708
+ process.stderr.write(`[taskbook-mcp] stdio transport connected (server: ${config.serverUrl})
709
+ `);
710
+ }
711
+
712
+ // src/transport/http.ts
713
+ import { randomUUID } from "crypto";
714
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
715
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
716
+ async function startHttpTransport(config) {
717
+ const sessions = new Map;
718
+ const { port, host } = config;
719
+ const requiredToken = process.env.TB_MCP_ACCESS_TOKEN;
720
+ function verifyAuth(req) {
721
+ if (!requiredToken)
722
+ return true;
723
+ const auth = req.headers.get("authorization");
724
+ if (!auth)
725
+ return false;
726
+ const [scheme, token] = auth.split(" ", 2);
727
+ return scheme?.toLowerCase() === "bearer" && token === requiredToken;
728
+ }
729
+ function corsHeaders() {
730
+ return {
731
+ "Access-Control-Allow-Origin": "*",
732
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
733
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, mcp-session-id, MCP-Protocol-Version",
734
+ "Access-Control-Expose-Headers": "mcp-session-id"
735
+ };
736
+ }
737
+ Bun.serve({
738
+ port,
739
+ hostname: host,
740
+ async fetch(req) {
741
+ const url = new URL(req.url);
742
+ if (url.pathname === "/health") {
743
+ return Response.json({
744
+ status: "ok",
745
+ server: SERVER_NAME,
746
+ version: SERVER_VERSION
747
+ });
748
+ }
749
+ if (url.pathname !== "/mcp") {
750
+ return new Response("Not Found", { status: 404 });
751
+ }
752
+ if (req.method === "OPTIONS") {
753
+ return new Response(null, { status: 204, headers: corsHeaders() });
754
+ }
755
+ if (!verifyAuth(req)) {
756
+ return Response.json({
757
+ jsonrpc: "2.0",
758
+ error: { code: -32001, message: "Unauthorized" },
759
+ id: null
760
+ }, { status: 401, headers: corsHeaders() });
761
+ }
762
+ if (req.method === "POST") {
763
+ const sessionId = req.headers.get("mcp-session-id");
764
+ if (sessionId && sessions.has(sessionId)) {
765
+ const session = sessions.get(sessionId);
766
+ return session.transport.handleRequest(req);
767
+ }
768
+ const body = await req.json();
769
+ if (isInitializeRequest(body)) {
770
+ const tbConfig = loadTaskbookConfig();
771
+ const client = new TaskbookClient(tbConfig);
772
+ const transport = new WebStandardStreamableHTTPServerTransport({
773
+ sessionIdGenerator: () => randomUUID(),
774
+ onsessioninitialized: (id) => {
775
+ sessions.set(id, { transport, client });
776
+ console.log(`[taskbook-mcp] HTTP session created: ${id}`);
777
+ }
778
+ });
779
+ transport.onclose = () => {
780
+ if (transport.sessionId) {
781
+ sessions.delete(transport.sessionId);
782
+ console.log(`[taskbook-mcp] HTTP session closed: ${transport.sessionId}`);
783
+ }
784
+ };
785
+ const mcpServer = createMcpServer(() => client);
786
+ await mcpServer.connect(transport);
787
+ return transport.handleRequest(req, { parsedBody: body });
788
+ }
789
+ return Response.json({
790
+ jsonrpc: "2.0",
791
+ error: { code: -32000, message: "Invalid or missing session" },
792
+ id: null
793
+ }, { status: 400, headers: corsHeaders() });
794
+ }
795
+ if (req.method === "GET") {
796
+ const sessionId = req.headers.get("mcp-session-id");
797
+ if (sessionId && sessions.has(sessionId)) {
798
+ return sessions.get(sessionId).transport.handleRequest(req);
799
+ }
800
+ return new Response("Invalid session", {
801
+ status: 400,
802
+ headers: corsHeaders()
803
+ });
804
+ }
805
+ if (req.method === "DELETE") {
806
+ const sessionId = req.headers.get("mcp-session-id");
807
+ if (sessionId && sessions.has(sessionId)) {
808
+ const session = sessions.get(sessionId);
809
+ await session.transport.close();
810
+ sessions.delete(sessionId);
811
+ return new Response(null, { status: 204, headers: corsHeaders() });
812
+ }
813
+ return new Response("Invalid session", {
814
+ status: 400,
815
+ headers: corsHeaders()
816
+ });
817
+ }
818
+ return new Response("Method Not Allowed", { status: 405 });
819
+ }
820
+ });
821
+ console.log(`[taskbook-mcp] HTTP transport listening on http://${host}:${port}/mcp`);
822
+ console.log(`[taskbook-mcp] Health check: http://${host}:${port}/health`);
823
+ }
824
+
825
+ // src/index.ts
826
+ async function main() {
827
+ const args = process.argv.slice(2);
828
+ let transportOverride;
829
+ for (const arg of args) {
830
+ if (arg.startsWith("--transport=")) {
831
+ transportOverride = arg.split("=")[1];
832
+ }
833
+ if (arg === "--help" || arg === "-h") {
834
+ console.log(`Taskbook MCP Server
835
+
836
+ Usage:
837
+ taskbook-mcp [--transport=stdio|http] [--port=PORT] [--host=HOST]
838
+
839
+ Environment variables:
840
+ TB_MCP_TRANSPORT Transport type: stdio (default) or http
841
+ TB_MCP_PORT HTTP port (default: 3100)
842
+ TB_MCP_HOST HTTP bind address (default: 127.0.0.1)
843
+ TB_MCP_ACCESS_TOKEN Optional: require this token for HTTP connections
844
+ TB_SERVER_URL Taskbook server URL (overrides ~/.taskbook.json)
845
+ TB_TOKEN Taskbook session token (overrides ~/.taskbook.json)
846
+ TB_ENCRYPTION_KEY Encryption key (overrides ~/.taskbook.json)
847
+ TB_CONFIG_PATH Path to taskbook config (default: ~/.taskbook.json)
848
+ `);
849
+ process.exit(0);
850
+ }
851
+ }
852
+ if (transportOverride) {
853
+ process.env.TB_MCP_TRANSPORT = transportOverride;
854
+ }
855
+ for (const arg of args) {
856
+ if (arg.startsWith("--port=")) {
857
+ process.env.TB_MCP_PORT = arg.split("=")[1];
858
+ }
859
+ if (arg.startsWith("--host=")) {
860
+ process.env.TB_MCP_HOST = arg.split("=")[1];
861
+ }
862
+ }
863
+ const config = loadMcpConfig();
864
+ if (config.transport === "http") {
865
+ await startHttpTransport(config);
866
+ } else {
867
+ await startStdioTransport();
868
+ }
869
+ }
870
+ main().catch((err) => {
871
+ console.error("[taskbook-mcp] Fatal error:", err.message ?? err);
872
+ process.exit(1);
873
+ });
874
+
875
+ //# debugId=A2D3326EC19207DD64756E2164756E21
876
+ //# sourceMappingURL=index.js.map