flowmind-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.
@@ -0,0 +1,906 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/config.ts
8
+ import { readFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ function loadConfig() {
12
+ const envKey = process.env.FLOWMIND_API_KEY;
13
+ const envUrl = process.env.FLOWMIND_BASE_URL;
14
+ if (envKey) {
15
+ return {
16
+ apiKey: envKey,
17
+ baseUrl: envUrl || "https://flowmind.life/api/v1"
18
+ };
19
+ }
20
+ try {
21
+ const configPath = join(homedir(), ".flowmind", "config.json");
22
+ const raw = readFileSync(configPath, "utf-8");
23
+ const config2 = JSON.parse(raw);
24
+ if (config2.api_key) {
25
+ return {
26
+ apiKey: config2.api_key,
27
+ baseUrl: config2.base_url || "https://flowmind.life/api/v1"
28
+ };
29
+ }
30
+ } catch {
31
+ }
32
+ throw new Error(
33
+ 'FlowMind API key not configured.\nSet FLOWMIND_API_KEY environment variable, or create ~/.flowmind/config.json:\n{"api_key": "fm_xxx", "base_url": "https://flowmind.life/api/v1"}'
34
+ );
35
+ }
36
+
37
+ // src/client.ts
38
+ import { readFileSync as readFileSync2 } from "fs";
39
+
40
+ // src/utils.ts
41
+ function formatList(result) {
42
+ const { data, meta } = result;
43
+ const { pagination } = meta;
44
+ const header = `Showing ${data.length} of ${pagination.total} (page ${pagination.page}/${pagination.totalPages})`;
45
+ return `${header}
46
+
47
+ ${JSON.stringify(data, null, 2)}`;
48
+ }
49
+ function formatItem(result) {
50
+ return JSON.stringify(result.data, null, 2);
51
+ }
52
+ function formatCreated(result, label) {
53
+ const id = result.data.id || "";
54
+ return `${label} created (ID: ${id})
55
+
56
+ ${JSON.stringify(result.data, null, 2)}`;
57
+ }
58
+ function formatUpdated(result, label) {
59
+ return `${label} updated
60
+
61
+ ${JSON.stringify(result.data, null, 2)}`;
62
+ }
63
+ function formatDeleted(label) {
64
+ return `${label} deleted successfully`;
65
+ }
66
+ function buildQueryString(params) {
67
+ const qs = new URLSearchParams();
68
+ for (const [key, value] of Object.entries(params)) {
69
+ if (value !== void 0 && value !== null) {
70
+ qs.set(key, String(value));
71
+ }
72
+ }
73
+ const str = qs.toString();
74
+ return str ? `?${str}` : "";
75
+ }
76
+ function toolResult(text, isError = false) {
77
+ return {
78
+ content: [{ type: "text", text }],
79
+ ...isError ? { isError: true } : {}
80
+ };
81
+ }
82
+ function errorResult(err) {
83
+ const message = err instanceof Error ? err.message : String(err);
84
+ return toolResult(`Error: ${message}`, true);
85
+ }
86
+
87
+ // src/client.ts
88
+ var FlowMindClient = class {
89
+ apiKey;
90
+ baseUrl;
91
+ constructor(config2) {
92
+ this.apiKey = config2.apiKey;
93
+ this.baseUrl = config2.baseUrl.replace(/\/$/, "");
94
+ }
95
+ async request(method, path, body) {
96
+ const url = `${this.baseUrl}${path}`;
97
+ const headers = {
98
+ "Authorization": `Bearer ${this.apiKey}`
99
+ };
100
+ const init = { method, headers };
101
+ if (body !== void 0) {
102
+ headers["Content-Type"] = "application/json";
103
+ init.body = JSON.stringify(body);
104
+ }
105
+ const res = await fetch(url, init);
106
+ if (res.status === 204) {
107
+ return void 0;
108
+ }
109
+ const json = await res.json();
110
+ if (!res.ok) {
111
+ const apiErr = json;
112
+ const msg = apiErr.error?.message || `API error ${res.status}`;
113
+ throw new Error(msg);
114
+ }
115
+ return json;
116
+ }
117
+ // ── Goals ──
118
+ async listGoals(params = {}) {
119
+ return this.request("GET", `/goals${buildQueryString(params)}`);
120
+ }
121
+ async getGoal(id) {
122
+ return this.request("GET", `/goals/${id}`);
123
+ }
124
+ async createGoal(data) {
125
+ return this.request("POST", "/goals", data);
126
+ }
127
+ async updateGoal(id, data) {
128
+ return this.request("PATCH", `/goals/${id}`, data);
129
+ }
130
+ async deleteGoal(id) {
131
+ await this.request("DELETE", `/goals/${id}`);
132
+ }
133
+ async listGoalTasks(goalId, params = {}) {
134
+ return this.request("GET", `/goals/${goalId}/tasks${buildQueryString(params)}`);
135
+ }
136
+ // ── Tasks ──
137
+ async listTasks(params = {}) {
138
+ return this.request("GET", `/tasks${buildQueryString(params)}`);
139
+ }
140
+ async getTask(id) {
141
+ return this.request("GET", `/tasks/${id}`);
142
+ }
143
+ async createTask(data) {
144
+ return this.request("POST", "/tasks", data);
145
+ }
146
+ async updateTask(id, data) {
147
+ return this.request("PATCH", `/tasks/${id}`, data);
148
+ }
149
+ async deleteTask(id) {
150
+ await this.request("DELETE", `/tasks/${id}`);
151
+ }
152
+ async listSubtasks(parentId, params = {}) {
153
+ return this.request("GET", `/tasks/${parentId}/subtasks${buildQueryString(params)}`);
154
+ }
155
+ async createSubtask(parentId, data) {
156
+ return this.request("POST", `/tasks/${parentId}/subtasks`, data);
157
+ }
158
+ // ── Notes ──
159
+ async listNotes(params = {}) {
160
+ return this.request("GET", `/notes${buildQueryString(params)}`);
161
+ }
162
+ async getNote(id) {
163
+ return this.request("GET", `/notes/${id}`);
164
+ }
165
+ async createNote(data) {
166
+ return this.request("POST", "/notes", data);
167
+ }
168
+ async updateNote(id, data) {
169
+ return this.request("PATCH", `/notes/${id}`, data);
170
+ }
171
+ async deleteNote(id) {
172
+ await this.request("DELETE", `/notes/${id}`);
173
+ }
174
+ // ── Note Images ──
175
+ async listNoteImages(noteId, params = {}) {
176
+ return this.request("GET", `/notes/${noteId}/images${buildQueryString(params)}`);
177
+ }
178
+ async uploadNoteImage(noteId, filePath, placeholderId, alt) {
179
+ const url = `${this.baseUrl}/notes/${noteId}/images`;
180
+ const fileBuffer = readFileSync2(filePath);
181
+ const fileName = filePath.split("/").pop() || "image.png";
182
+ const ext = fileName.split(".").pop()?.toLowerCase() || "png";
183
+ const mimeMap = {
184
+ jpg: "image/jpeg",
185
+ jpeg: "image/jpeg",
186
+ png: "image/png",
187
+ gif: "image/gif",
188
+ webp: "image/webp"
189
+ };
190
+ const mimeType = mimeMap[ext] || "image/png";
191
+ const formData = new FormData();
192
+ formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
193
+ if (placeholderId) formData.append("placeholder_id", placeholderId);
194
+ if (alt) formData.append("alt", alt);
195
+ const res = await fetch(url, {
196
+ method: "POST",
197
+ headers: { "Authorization": `Bearer ${this.apiKey}` },
198
+ body: formData
199
+ });
200
+ const json = await res.json();
201
+ if (!res.ok) {
202
+ const apiErr = json;
203
+ throw new Error(apiErr.error?.message || `Upload failed: ${res.status}`);
204
+ }
205
+ return json;
206
+ }
207
+ async deleteNoteImage(noteId, imageId) {
208
+ await this.request("DELETE", `/notes/${noteId}/images/${imageId}`);
209
+ }
210
+ // ── People ──
211
+ async listPeople(params = {}) {
212
+ return this.request("GET", `/people${buildQueryString(params)}`);
213
+ }
214
+ async getPerson(id) {
215
+ return this.request("GET", `/people/${id}`);
216
+ }
217
+ async createPerson(data) {
218
+ return this.request("POST", "/people", data);
219
+ }
220
+ async updatePerson(id, data) {
221
+ return this.request("PATCH", `/people/${id}`, data);
222
+ }
223
+ async deletePerson(id) {
224
+ await this.request("DELETE", `/people/${id}`);
225
+ }
226
+ async listPersonTags(personId) {
227
+ return this.request("GET", `/people/${personId}/tags`);
228
+ }
229
+ async addPersonTag(personId, tagId) {
230
+ return this.request("POST", `/people/${personId}/tags`, { tag_id: tagId });
231
+ }
232
+ async removePersonTag(personId, tagId) {
233
+ await this.request("DELETE", `/people/${personId}/tags/${tagId}`);
234
+ }
235
+ // ── Tags ──
236
+ async listTags(params = {}) {
237
+ return this.request("GET", `/tags${buildQueryString(params)}`);
238
+ }
239
+ async getTag(id) {
240
+ return this.request("GET", `/tags/${id}`);
241
+ }
242
+ async createTag(data) {
243
+ return this.request("POST", "/tags", data);
244
+ }
245
+ async updateTag(id, data) {
246
+ return this.request("PATCH", `/tags/${id}`, data);
247
+ }
248
+ async deleteTag(id) {
249
+ await this.request("DELETE", `/tags/${id}`);
250
+ }
251
+ };
252
+
253
+ // src/tools/goals.ts
254
+ import { z } from "zod";
255
+ function registerGoalTools(server2, client2) {
256
+ server2.tool(
257
+ "list_goals",
258
+ "List goals with optional filters for status, category, and pinned state. Returns paginated results.",
259
+ {
260
+ status: z.enum(["active", "completed", "archived"]).optional().describe("Filter by goal status"),
261
+ category: z.enum(["business", "career", "health", "personal", "learning", "financial"]).optional().describe("Filter by category"),
262
+ pinned: z.boolean().optional().describe("Filter by pinned state"),
263
+ sort: z.enum(["created_at", "updated_at", "title", "target_date", "progress"]).optional().describe("Sort field"),
264
+ order: z.enum(["asc", "desc"]).optional().describe("Sort direction"),
265
+ page: z.number().int().min(1).optional().describe("Page number"),
266
+ limit: z.number().int().min(1).max(100).optional().describe("Items per page")
267
+ },
268
+ async (params) => {
269
+ try {
270
+ const result = await client2.listGoals(params);
271
+ return toolResult(formatList(result));
272
+ } catch (err) {
273
+ return errorResult(err);
274
+ }
275
+ }
276
+ );
277
+ server2.tool(
278
+ "get_goal",
279
+ "Get the details of a specific goal by its ID.",
280
+ { id: z.string().describe("Goal ID (UUID)") },
281
+ async ({ id }) => {
282
+ try {
283
+ const result = await client2.getGoal(id);
284
+ return toolResult(formatItem(result));
285
+ } catch (err) {
286
+ return errorResult(err);
287
+ }
288
+ }
289
+ );
290
+ server2.tool(
291
+ "create_goal",
292
+ "Create a new goal. Goals help you organize tasks under high-level objectives.",
293
+ {
294
+ title: z.string().describe("Goal title (required)"),
295
+ description: z.string().optional().describe("Goal description"),
296
+ category: z.enum(["business", "career", "health", "personal", "learning", "financial"]).optional().describe("Category"),
297
+ target_date: z.string().optional().describe("Target date (YYYY-MM-DD)"),
298
+ status: z.enum(["active", "completed", "archived"]).optional().describe("Status"),
299
+ progress: z.number().int().min(0).max(100).optional().describe("Progress percentage (0-100)"),
300
+ pinned: z.boolean().optional().describe("Pin this goal")
301
+ },
302
+ async (params) => {
303
+ try {
304
+ const result = await client2.createGoal(params);
305
+ return toolResult(formatCreated(result, "Goal"));
306
+ } catch (err) {
307
+ return errorResult(err);
308
+ }
309
+ }
310
+ );
311
+ server2.tool(
312
+ "update_goal",
313
+ "Update one or more fields on an existing goal.",
314
+ {
315
+ id: z.string().describe("Goal ID (UUID)"),
316
+ title: z.string().optional().describe("New title"),
317
+ description: z.string().optional().describe("New description"),
318
+ category: z.enum(["business", "career", "health", "personal", "learning", "financial"]).optional(),
319
+ target_date: z.string().optional().describe("New target date (YYYY-MM-DD)"),
320
+ status: z.enum(["active", "completed", "archived"]).optional(),
321
+ progress: z.number().int().min(0).max(100).optional(),
322
+ pinned: z.boolean().optional()
323
+ },
324
+ async ({ id, ...data }) => {
325
+ try {
326
+ const result = await client2.updateGoal(id, data);
327
+ return toolResult(formatUpdated(result, "Goal"));
328
+ } catch (err) {
329
+ return errorResult(err);
330
+ }
331
+ }
332
+ );
333
+ server2.tool(
334
+ "delete_goal",
335
+ "Permanently delete a goal. Associated tasks are unlinked, not deleted.",
336
+ { id: z.string().describe("Goal ID (UUID)") },
337
+ async ({ id }) => {
338
+ try {
339
+ await client2.deleteGoal(id);
340
+ return toolResult(formatDeleted("Goal"));
341
+ } catch (err) {
342
+ return errorResult(err);
343
+ }
344
+ }
345
+ );
346
+ server2.tool(
347
+ "list_goal_tasks",
348
+ "List all tasks associated with a specific goal.",
349
+ {
350
+ goal_id: z.string().describe("Goal ID (UUID)"),
351
+ status: z.enum(["todo", "in_progress", "completed", "archived"]).optional(),
352
+ priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
353
+ page: z.number().int().min(1).optional(),
354
+ limit: z.number().int().min(1).max(100).optional()
355
+ },
356
+ async ({ goal_id, ...params }) => {
357
+ try {
358
+ const result = await client2.listGoalTasks(goal_id, params);
359
+ return toolResult(formatList(result));
360
+ } catch (err) {
361
+ return errorResult(err);
362
+ }
363
+ }
364
+ );
365
+ }
366
+
367
+ // src/tools/tasks.ts
368
+ import { z as z2 } from "zod";
369
+ function registerTaskTools(server2, client2) {
370
+ server2.tool(
371
+ "list_tasks",
372
+ "List tasks with optional filters. Supports filtering by status, priority, energy level, goal, person, due date range, and focus state.",
373
+ {
374
+ status: z2.enum(["todo", "in_progress", "completed", "archived"]).optional(),
375
+ priority: z2.enum(["low", "medium", "high", "urgent"]).optional(),
376
+ energy_level: z2.enum(["low", "medium", "high"]).optional(),
377
+ goal_id: z2.string().optional().describe("Filter by goal ID"),
378
+ person_id: z2.string().optional().describe("Filter by person ID"),
379
+ parent_task_id: z2.string().optional().describe('Filter by parent task. Use "null" for root tasks only.'),
380
+ due_date_from: z2.string().optional().describe("Due date from (YYYY-MM-DD)"),
381
+ due_date_to: z2.string().optional().describe("Due date to (YYYY-MM-DD)"),
382
+ pinned: z2.boolean().optional(),
383
+ focused: z2.boolean().optional(),
384
+ focus_today: z2.boolean().optional(),
385
+ sort: z2.enum(["created_at", "updated_at", "due_date", "priority", "title"]).optional(),
386
+ order: z2.enum(["asc", "desc"]).optional(),
387
+ page: z2.number().int().min(1).optional(),
388
+ limit: z2.number().int().min(1).max(100).optional()
389
+ },
390
+ async (params) => {
391
+ try {
392
+ const result = await client2.listTasks(params);
393
+ return toolResult(formatList(result));
394
+ } catch (err) {
395
+ return errorResult(err);
396
+ }
397
+ }
398
+ );
399
+ server2.tool(
400
+ "get_task",
401
+ "Get the details of a specific task including priority, energy level, due date, and linked goal/person.",
402
+ { id: z2.string().describe("Task ID (UUID)") },
403
+ async ({ id }) => {
404
+ try {
405
+ const result = await client2.getTask(id);
406
+ return toolResult(formatItem(result));
407
+ } catch (err) {
408
+ return errorResult(err);
409
+ }
410
+ }
411
+ );
412
+ server2.tool(
413
+ "create_task",
414
+ "Create a new task. Tasks can be linked to goals, assigned to people, and organized with priorities and due dates.",
415
+ {
416
+ title: z2.string().describe("Task title (required)"),
417
+ description: z2.string().optional(),
418
+ status: z2.enum(["todo", "in_progress", "completed", "archived"]).optional(),
419
+ priority: z2.enum(["low", "medium", "high", "urgent"]).optional(),
420
+ energy_level: z2.enum(["low", "medium", "high"]).optional(),
421
+ due_date: z2.string().optional().describe("Due date (YYYY-MM-DD)"),
422
+ goal_id: z2.string().optional().describe("Link to a goal"),
423
+ person_id: z2.string().optional().describe("Assign to a person"),
424
+ estimated_minutes: z2.number().int().optional(),
425
+ pinned: z2.boolean().optional(),
426
+ focus_today: z2.boolean().optional()
427
+ },
428
+ async (params) => {
429
+ try {
430
+ const result = await client2.createTask(params);
431
+ return toolResult(formatCreated(result, "Task"));
432
+ } catch (err) {
433
+ return errorResult(err);
434
+ }
435
+ }
436
+ );
437
+ server2.tool(
438
+ "update_task",
439
+ "Update one or more fields on an existing task.",
440
+ {
441
+ id: z2.string().describe("Task ID (UUID)"),
442
+ title: z2.string().optional(),
443
+ description: z2.string().optional(),
444
+ status: z2.enum(["todo", "in_progress", "completed", "archived"]).optional(),
445
+ priority: z2.enum(["low", "medium", "high", "urgent"]).optional(),
446
+ energy_level: z2.enum(["low", "medium", "high"]).optional(),
447
+ due_date: z2.string().optional(),
448
+ goal_id: z2.string().nullable().optional(),
449
+ person_id: z2.string().nullable().optional(),
450
+ estimated_minutes: z2.number().int().nullable().optional(),
451
+ pinned: z2.boolean().optional(),
452
+ focused: z2.boolean().optional(),
453
+ focus_today: z2.boolean().optional()
454
+ },
455
+ async ({ id, ...data }) => {
456
+ try {
457
+ const result = await client2.updateTask(id, data);
458
+ return toolResult(formatUpdated(result, "Task"));
459
+ } catch (err) {
460
+ return errorResult(err);
461
+ }
462
+ }
463
+ );
464
+ server2.tool(
465
+ "delete_task",
466
+ "Permanently delete a task and all its subtasks.",
467
+ { id: z2.string().describe("Task ID (UUID)") },
468
+ async ({ id }) => {
469
+ try {
470
+ await client2.deleteTask(id);
471
+ return toolResult(formatDeleted("Task"));
472
+ } catch (err) {
473
+ return errorResult(err);
474
+ }
475
+ }
476
+ );
477
+ server2.tool(
478
+ "list_subtasks",
479
+ "List subtasks (child tasks) for a specific parent task.",
480
+ {
481
+ parent_task_id: z2.string().describe("Parent task ID (UUID)"),
482
+ page: z2.number().int().min(1).optional(),
483
+ limit: z2.number().int().min(1).max(100).optional()
484
+ },
485
+ async ({ parent_task_id, ...params }) => {
486
+ try {
487
+ const result = await client2.listSubtasks(parent_task_id, params);
488
+ return toolResult(formatList(result));
489
+ } catch (err) {
490
+ return errorResult(err);
491
+ }
492
+ }
493
+ );
494
+ server2.tool(
495
+ "create_subtask",
496
+ "Create a new subtask under the specified parent task.",
497
+ {
498
+ parent_task_id: z2.string().describe("Parent task ID (UUID)"),
499
+ title: z2.string().describe("Subtask title (required)"),
500
+ description: z2.string().optional(),
501
+ priority: z2.enum(["low", "medium", "high", "urgent"]).optional(),
502
+ energy_level: z2.enum(["low", "medium", "high"]).optional(),
503
+ due_date: z2.string().optional(),
504
+ estimated_minutes: z2.number().int().optional()
505
+ },
506
+ async ({ parent_task_id, ...data }) => {
507
+ try {
508
+ const result = await client2.createSubtask(parent_task_id, data);
509
+ return toolResult(formatCreated(result, "Subtask"));
510
+ } catch (err) {
511
+ return errorResult(err);
512
+ }
513
+ }
514
+ );
515
+ }
516
+
517
+ // src/tools/notes.ts
518
+ import { z as z3 } from "zod";
519
+ function registerNoteTools(server2, client2) {
520
+ server2.tool(
521
+ "list_notes",
522
+ "List notes with optional filters. Note content with embedded images will have signed URLs automatically refreshed.",
523
+ {
524
+ category: z3.string().optional().describe("Filter by category"),
525
+ task_id: z3.string().optional().describe("Filter by linked task ID"),
526
+ pinned: z3.boolean().optional(),
527
+ sort: z3.enum(["created_at", "updated_at", "title"]).optional(),
528
+ order: z3.enum(["asc", "desc"]).optional(),
529
+ page: z3.number().int().min(1).optional(),
530
+ limit: z3.number().int().min(1).max(100).optional()
531
+ },
532
+ async (params) => {
533
+ try {
534
+ const result = await client2.listNotes(params);
535
+ return toolResult(formatList(result));
536
+ } catch (err) {
537
+ return errorResult(err);
538
+ }
539
+ }
540
+ );
541
+ server2.tool(
542
+ "get_note",
543
+ "Get a note by ID. Embedded image URLs are automatically refreshed.",
544
+ { id: z3.string().describe("Note ID (UUID)") },
545
+ async ({ id }) => {
546
+ try {
547
+ const result = await client2.getNote(id);
548
+ return toolResult(formatItem(result));
549
+ } catch (err) {
550
+ return errorResult(err);
551
+ }
552
+ }
553
+ );
554
+ server2.tool(
555
+ "create_note",
556
+ "Create a new note. Content can include {{IMG:placeholder_id}} markers for image insertion \u2014 upload images via upload_note_image to replace them.",
557
+ {
558
+ title: z3.string().describe("Note title (required)"),
559
+ content: z3.string().optional().describe("Note content (supports Markdown and {{IMG:id}} placeholders)"),
560
+ category: z3.string().optional(),
561
+ task_id: z3.string().optional().describe("Link to a task"),
562
+ is_protected: z3.boolean().optional(),
563
+ pinned: z3.boolean().optional()
564
+ },
565
+ async (params) => {
566
+ try {
567
+ const result = await client2.createNote(params);
568
+ return toolResult(formatCreated(result, "Note"));
569
+ } catch (err) {
570
+ return errorResult(err);
571
+ }
572
+ }
573
+ );
574
+ server2.tool(
575
+ "update_note",
576
+ "Update one or more fields on an existing note. Existing embedded images are preserved.",
577
+ {
578
+ id: z3.string().describe("Note ID (UUID)"),
579
+ title: z3.string().optional(),
580
+ content: z3.string().optional(),
581
+ category: z3.string().nullable().optional(),
582
+ task_id: z3.string().nullable().optional(),
583
+ is_protected: z3.boolean().optional(),
584
+ pinned: z3.boolean().optional()
585
+ },
586
+ async ({ id, ...data }) => {
587
+ try {
588
+ const result = await client2.updateNote(id, data);
589
+ return toolResult(formatUpdated(result, "Note"));
590
+ } catch (err) {
591
+ return errorResult(err);
592
+ }
593
+ }
594
+ );
595
+ server2.tool(
596
+ "delete_note",
597
+ "Permanently delete a note and all its image attachments.",
598
+ { id: z3.string().describe("Note ID (UUID)") },
599
+ async ({ id }) => {
600
+ try {
601
+ await client2.deleteNote(id);
602
+ return toolResult(formatDeleted("Note"));
603
+ } catch (err) {
604
+ return errorResult(err);
605
+ }
606
+ }
607
+ );
608
+ server2.tool(
609
+ "list_note_images",
610
+ "List image attachments for a note, each with a fresh signed URL.",
611
+ {
612
+ note_id: z3.string().describe("Note ID (UUID)"),
613
+ page: z3.number().int().min(1).optional(),
614
+ limit: z3.number().int().min(1).max(100).optional()
615
+ },
616
+ async ({ note_id, ...params }) => {
617
+ try {
618
+ const result = await client2.listNoteImages(note_id, params);
619
+ return toolResult(formatList(result));
620
+ } catch (err) {
621
+ return errorResult(err);
622
+ }
623
+ }
624
+ );
625
+ server2.tool(
626
+ "upload_note_image",
627
+ "Upload an image to a note. If placeholder_id matches a {{IMG:id}} marker in note content, it is replaced with an <img> tag. Max 25MB, JPEG/PNG/GIF/WebP.",
628
+ {
629
+ note_id: z3.string().describe("Note ID (UUID)"),
630
+ file_path: z3.string().describe("Local path to the image file"),
631
+ placeholder_id: z3.string().optional().describe("Placeholder ID to replace in note content"),
632
+ alt: z3.string().optional().describe("Alt text for the image")
633
+ },
634
+ async ({ note_id, file_path, placeholder_id, alt }) => {
635
+ try {
636
+ const result = await client2.uploadNoteImage(note_id, file_path, placeholder_id, alt);
637
+ const data = result.data;
638
+ const status = data.replaced ? "uploaded and placeholder replaced" : "uploaded as attachment";
639
+ return toolResult(`Image ${status} (ID: ${data.id})
640
+
641
+ ${JSON.stringify(data, null, 2)}`);
642
+ } catch (err) {
643
+ return errorResult(err);
644
+ }
645
+ }
646
+ );
647
+ server2.tool(
648
+ "delete_note_image",
649
+ "Delete an image attachment and remove its <img> tag from note content.",
650
+ {
651
+ note_id: z3.string().describe("Note ID (UUID)"),
652
+ image_id: z3.string().describe("Image attachment ID (UUID)")
653
+ },
654
+ async ({ note_id, image_id }) => {
655
+ try {
656
+ await client2.deleteNoteImage(note_id, image_id);
657
+ return toolResult(formatDeleted("Note image"));
658
+ } catch (err) {
659
+ return errorResult(err);
660
+ }
661
+ }
662
+ );
663
+ }
664
+
665
+ // src/tools/people.ts
666
+ import { z as z4 } from "zod";
667
+ function registerPeopleTools(server2, client2) {
668
+ server2.tool(
669
+ "list_people",
670
+ "List contacts with optional filters for relationship type, tag, or search.",
671
+ {
672
+ relationship_type: z4.enum(["business", "colleague", "friend", "family", "mentor", "client", "partner", "other"]).optional(),
673
+ tag_id: z4.string().optional().describe("Filter by tag ID"),
674
+ search: z4.string().optional().describe("Search by name, email, or company"),
675
+ sort: z4.enum(["created_at", "updated_at", "name"]).optional(),
676
+ order: z4.enum(["asc", "desc"]).optional(),
677
+ page: z4.number().int().min(1).optional(),
678
+ limit: z4.number().int().min(1).max(100).optional()
679
+ },
680
+ async (params) => {
681
+ try {
682
+ const result = await client2.listPeople(params);
683
+ return toolResult(formatList(result));
684
+ } catch (err) {
685
+ return errorResult(err);
686
+ }
687
+ }
688
+ );
689
+ server2.tool(
690
+ "get_person",
691
+ "Get the details of a specific person by their ID.",
692
+ { id: z4.string().describe("Person ID (UUID)") },
693
+ async ({ id }) => {
694
+ try {
695
+ const result = await client2.getPerson(id);
696
+ return toolResult(formatItem(result));
697
+ } catch (err) {
698
+ return errorResult(err);
699
+ }
700
+ }
701
+ );
702
+ server2.tool(
703
+ "create_person",
704
+ "Create a new contact. People can be linked to tasks and tagged for organization.",
705
+ {
706
+ name: z4.string().describe("Person name (required)"),
707
+ email: z4.string().optional(),
708
+ phone: z4.string().optional(),
709
+ company: z4.string().optional(),
710
+ role: z4.string().optional().describe("Job title"),
711
+ relationship_type: z4.enum(["business", "colleague", "friend", "family", "mentor", "client", "partner", "other"]).optional(),
712
+ notes: z4.string().optional(),
713
+ location: z4.string().optional(),
714
+ birthday: z4.string().optional().describe("Birthday (YYYY-MM-DD)")
715
+ },
716
+ async (params) => {
717
+ try {
718
+ const result = await client2.createPerson(params);
719
+ return toolResult(formatCreated(result, "Person"));
720
+ } catch (err) {
721
+ return errorResult(err);
722
+ }
723
+ }
724
+ );
725
+ server2.tool(
726
+ "update_person",
727
+ "Update one or more fields on an existing person.",
728
+ {
729
+ id: z4.string().describe("Person ID (UUID)"),
730
+ name: z4.string().optional(),
731
+ email: z4.string().nullable().optional(),
732
+ phone: z4.string().nullable().optional(),
733
+ company: z4.string().nullable().optional(),
734
+ role: z4.string().nullable().optional(),
735
+ relationship_type: z4.enum(["business", "colleague", "friend", "family", "mentor", "client", "partner", "other"]).optional(),
736
+ notes: z4.string().nullable().optional(),
737
+ location: z4.string().nullable().optional()
738
+ },
739
+ async ({ id, ...data }) => {
740
+ try {
741
+ const result = await client2.updatePerson(id, data);
742
+ return toolResult(formatUpdated(result, "Person"));
743
+ } catch (err) {
744
+ return errorResult(err);
745
+ }
746
+ }
747
+ );
748
+ server2.tool(
749
+ "delete_person",
750
+ "Permanently delete a person and remove all their tag associations.",
751
+ { id: z4.string().describe("Person ID (UUID)") },
752
+ async ({ id }) => {
753
+ try {
754
+ await client2.deletePerson(id);
755
+ return toolResult(formatDeleted("Person"));
756
+ } catch (err) {
757
+ return errorResult(err);
758
+ }
759
+ }
760
+ );
761
+ server2.tool(
762
+ "list_person_tags",
763
+ "List all tags assigned to a specific person.",
764
+ { person_id: z4.string().describe("Person ID (UUID)") },
765
+ async ({ person_id }) => {
766
+ try {
767
+ const result = await client2.listPersonTags(person_id);
768
+ return toolResult(JSON.stringify(result.data, null, 2));
769
+ } catch (err) {
770
+ return errorResult(err);
771
+ }
772
+ }
773
+ );
774
+ server2.tool(
775
+ "add_person_tag",
776
+ "Assign an existing tag to a person. Returns 409 if already assigned.",
777
+ {
778
+ person_id: z4.string().describe("Person ID (UUID)"),
779
+ tag_id: z4.string().describe("Tag ID (UUID)")
780
+ },
781
+ async ({ person_id, tag_id }) => {
782
+ try {
783
+ const result = await client2.addPersonTag(person_id, tag_id);
784
+ return toolResult(`Tag assigned
785
+
786
+ ${JSON.stringify(result.data, null, 2)}`);
787
+ } catch (err) {
788
+ return errorResult(err);
789
+ }
790
+ }
791
+ );
792
+ server2.tool(
793
+ "remove_person_tag",
794
+ "Remove a tag from a person. The tag itself is not deleted.",
795
+ {
796
+ person_id: z4.string().describe("Person ID (UUID)"),
797
+ tag_id: z4.string().describe("Tag ID (UUID)")
798
+ },
799
+ async ({ person_id, tag_id }) => {
800
+ try {
801
+ await client2.removePersonTag(person_id, tag_id);
802
+ return toolResult("Tag removed from person");
803
+ } catch (err) {
804
+ return errorResult(err);
805
+ }
806
+ }
807
+ );
808
+ }
809
+
810
+ // src/tools/tags.ts
811
+ import { z as z5 } from "zod";
812
+ function registerTagTools(server2, client2) {
813
+ server2.tool(
814
+ "list_tags",
815
+ "List all tags. Tags are used to categorize people.",
816
+ {
817
+ sort: z5.enum(["name", "created_at"]).optional(),
818
+ order: z5.enum(["asc", "desc"]).optional(),
819
+ page: z5.number().int().min(1).optional(),
820
+ limit: z5.number().int().min(1).max(100).optional()
821
+ },
822
+ async (params) => {
823
+ try {
824
+ const result = await client2.listTags(params);
825
+ return toolResult(formatList(result));
826
+ } catch (err) {
827
+ return errorResult(err);
828
+ }
829
+ }
830
+ );
831
+ server2.tool(
832
+ "get_tag",
833
+ "Get the details of a specific tag by its ID.",
834
+ { id: z5.string().describe("Tag ID (UUID)") },
835
+ async ({ id }) => {
836
+ try {
837
+ const result = await client2.getTag(id);
838
+ return toolResult(formatItem(result));
839
+ } catch (err) {
840
+ return errorResult(err);
841
+ }
842
+ }
843
+ );
844
+ server2.tool(
845
+ "create_tag",
846
+ "Create a new tag. Tag names must be unique \u2014 returns 409 if a tag with the same name exists.",
847
+ {
848
+ name: z5.string().describe("Tag name (required, must be unique)"),
849
+ color: z5.string().optional().describe("Tag color (hex code, e.g. #ff0000)")
850
+ },
851
+ async (params) => {
852
+ try {
853
+ const result = await client2.createTag(params);
854
+ return toolResult(formatCreated(result, "Tag"));
855
+ } catch (err) {
856
+ return errorResult(err);
857
+ }
858
+ }
859
+ );
860
+ server2.tool(
861
+ "update_tag",
862
+ "Update a tag name or color. Returns 409 if the new name conflicts with an existing tag.",
863
+ {
864
+ id: z5.string().describe("Tag ID (UUID)"),
865
+ name: z5.string().optional(),
866
+ color: z5.string().optional()
867
+ },
868
+ async ({ id, ...data }) => {
869
+ try {
870
+ const result = await client2.updateTag(id, data);
871
+ return toolResult(formatUpdated(result, "Tag"));
872
+ } catch (err) {
873
+ return errorResult(err);
874
+ }
875
+ }
876
+ );
877
+ server2.tool(
878
+ "delete_tag",
879
+ "Permanently delete a tag and remove it from all people.",
880
+ { id: z5.string().describe("Tag ID (UUID)") },
881
+ async ({ id }) => {
882
+ try {
883
+ await client2.deleteTag(id);
884
+ return toolResult(formatDeleted("Tag"));
885
+ } catch (err) {
886
+ return errorResult(err);
887
+ }
888
+ }
889
+ );
890
+ }
891
+
892
+ // src/index.ts
893
+ var config = loadConfig();
894
+ var client = new FlowMindClient(config);
895
+ var server = new McpServer({
896
+ name: "flowmind",
897
+ version: "1.0.0"
898
+ });
899
+ registerGoalTools(server, client);
900
+ registerTaskTools(server, client);
901
+ registerNoteTools(server, client);
902
+ registerPeopleTools(server, client);
903
+ registerTagTools(server, client);
904
+ var transport = new StdioServerTransport();
905
+ await server.connect(transport);
906
+ //# sourceMappingURL=flowmind-mcp.js.map