mindpm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,981 @@
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/tools/projects.ts
8
+ import { z } from "zod/v4";
9
+
10
+ // src/db/connection.ts
11
+ import Database from "better-sqlite3";
12
+ import { mkdirSync } from "fs";
13
+ import { dirname, resolve } from "path";
14
+ import { homedir } from "os";
15
+
16
+ // src/db/schema.ts
17
+ function createSchema(db2) {
18
+ db2.exec(`
19
+ CREATE TABLE IF NOT EXISTS projects (
20
+ id TEXT PRIMARY KEY,
21
+ name TEXT NOT NULL UNIQUE,
22
+ description TEXT,
23
+ status TEXT DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'archived')),
24
+ repo_path TEXT,
25
+ tech_stack TEXT,
26
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
27
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS tasks (
31
+ id TEXT PRIMARY KEY,
32
+ project_id TEXT NOT NULL REFERENCES projects(id),
33
+ title TEXT NOT NULL,
34
+ description TEXT,
35
+ status TEXT DEFAULT 'todo' CHECK(status IN ('todo', 'in_progress', 'blocked', 'done', 'cancelled')),
36
+ priority TEXT DEFAULT 'medium' CHECK(priority IN ('critical', 'high', 'medium', 'low')),
37
+ tags TEXT,
38
+ parent_task_id TEXT REFERENCES tasks(id),
39
+ blocked_by TEXT,
40
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
41
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
42
+ completed_at DATETIME
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS decisions (
46
+ id TEXT PRIMARY KEY,
47
+ project_id TEXT NOT NULL REFERENCES projects(id),
48
+ title TEXT NOT NULL,
49
+ decision TEXT NOT NULL,
50
+ reasoning TEXT,
51
+ alternatives TEXT,
52
+ tags TEXT,
53
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS notes (
57
+ id TEXT PRIMARY KEY,
58
+ project_id TEXT NOT NULL REFERENCES projects(id),
59
+ task_id TEXT REFERENCES tasks(id),
60
+ content TEXT NOT NULL,
61
+ category TEXT DEFAULT 'general' CHECK(category IN ('general', 'architecture', 'bug', 'idea', 'research', 'meeting', 'review')),
62
+ tags TEXT,
63
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS sessions (
67
+ id TEXT PRIMARY KEY,
68
+ project_id TEXT NOT NULL REFERENCES projects(id),
69
+ summary TEXT NOT NULL,
70
+ tasks_worked_on TEXT,
71
+ decisions_made TEXT,
72
+ next_steps TEXT,
73
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS context (
77
+ id TEXT PRIMARY KEY,
78
+ project_id TEXT NOT NULL REFERENCES projects(id),
79
+ key TEXT NOT NULL,
80
+ value TEXT NOT NULL,
81
+ category TEXT DEFAULT 'general',
82
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
83
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
84
+ UNIQUE(project_id, key)
85
+ );
86
+
87
+ -- Indexes
88
+ CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
89
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
90
+ CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
91
+ CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at);
92
+
93
+ CREATE INDEX IF NOT EXISTS idx_decisions_project_id ON decisions(project_id);
94
+ CREATE INDEX IF NOT EXISTS idx_decisions_created_at ON decisions(created_at);
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_notes_project_id ON notes(project_id);
97
+ CREATE INDEX IF NOT EXISTS idx_notes_task_id ON notes(task_id);
98
+ CREATE INDEX IF NOT EXISTS idx_notes_category ON notes(category);
99
+ CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
100
+
101
+ CREATE INDEX IF NOT EXISTS idx_sessions_project_id ON sessions(project_id);
102
+ CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON sessions(created_at);
103
+
104
+ CREATE INDEX IF NOT EXISTS idx_context_project_id ON context(project_id);
105
+
106
+ -- Triggers for updated_at (WHEN clause prevents infinite recursion)
107
+ CREATE TRIGGER IF NOT EXISTS trg_projects_updated_at
108
+ AFTER UPDATE ON projects
109
+ FOR EACH ROW
110
+ WHEN NEW.updated_at = OLD.updated_at
111
+ BEGIN
112
+ UPDATE projects SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
113
+ END;
114
+
115
+ CREATE TRIGGER IF NOT EXISTS trg_tasks_updated_at
116
+ AFTER UPDATE ON tasks
117
+ FOR EACH ROW
118
+ WHEN NEW.updated_at = OLD.updated_at
119
+ BEGIN
120
+ UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
121
+ END;
122
+
123
+ CREATE TRIGGER IF NOT EXISTS trg_context_updated_at
124
+ AFTER UPDATE ON context
125
+ FOR EACH ROW
126
+ WHEN NEW.updated_at = OLD.updated_at
127
+ BEGIN
128
+ UPDATE context SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
129
+ END;
130
+ `);
131
+ }
132
+
133
+ // src/db/connection.ts
134
+ var db = null;
135
+ function resolveDbPath() {
136
+ const envPath = process.env.MINDPM_DB_PATH || process.env.PROJECT_MEMORY_DB_PATH;
137
+ if (envPath) {
138
+ return envPath.replace(/^~/, homedir());
139
+ }
140
+ return resolve(homedir(), ".mindpm", "memory.db");
141
+ }
142
+ function getDb() {
143
+ if (db) return db;
144
+ const dbPath = resolveDbPath();
145
+ mkdirSync(dirname(dbPath), { recursive: true });
146
+ db = new Database(dbPath);
147
+ db.pragma("journal_mode = WAL");
148
+ db.pragma("foreign_keys = ON");
149
+ createSchema(db);
150
+ return db;
151
+ }
152
+ function closeDb() {
153
+ if (db) {
154
+ db.close();
155
+ db = null;
156
+ }
157
+ }
158
+
159
+ // src/utils/ids.ts
160
+ import { randomBytes } from "crypto";
161
+ function generateId() {
162
+ return randomBytes(4).toString("hex");
163
+ }
164
+
165
+ // src/db/queries.ts
166
+ function resolveProjectId(projectRef) {
167
+ const db2 = getDb();
168
+ const byId = db2.prepare("SELECT id FROM projects WHERE id = ?").get(projectRef);
169
+ if (byId) return byId.id;
170
+ const byName = db2.prepare("SELECT id FROM projects WHERE LOWER(name) = LOWER(?)").get(projectRef);
171
+ if (byName) return byName.id;
172
+ return null;
173
+ }
174
+ function getMostRecentProject() {
175
+ const db2 = getDb();
176
+ const row = db2.prepare(
177
+ `SELECT id, name FROM projects WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1`
178
+ ).get();
179
+ return row ?? null;
180
+ }
181
+ function resolveProjectOrDefault(projectRef) {
182
+ const db2 = getDb();
183
+ if (projectRef) {
184
+ const id = resolveProjectId(projectRef);
185
+ if (!id) return null;
186
+ const row = db2.prepare("SELECT id, name FROM projects WHERE id = ?").get(id);
187
+ return row ?? null;
188
+ }
189
+ return getMostRecentProject();
190
+ }
191
+
192
+ // src/tools/projects.ts
193
+ function registerProjectTools(server2) {
194
+ server2.registerTool(
195
+ "create_project",
196
+ {
197
+ title: "Create Project",
198
+ description: "Create a new project to track. Use this when starting a new project or when a user mentions a project that doesn't exist yet.",
199
+ inputSchema: {
200
+ name: z.string().describe("Project name (unique)"),
201
+ description: z.string().optional().describe("What this project is about"),
202
+ tech_stack: z.array(z.string()).optional().describe('Technologies used, e.g. ["FastAPI", "React", "PostgreSQL"]'),
203
+ repo_path: z.string().optional().describe("Path to the project repository")
204
+ }
205
+ },
206
+ async ({ name, description, tech_stack, repo_path }) => {
207
+ const db2 = getDb();
208
+ const id = generateId();
209
+ try {
210
+ db2.prepare(
211
+ `INSERT INTO projects (id, name, description, tech_stack, repo_path) VALUES (?, ?, ?, ?, ?)`
212
+ ).run(id, name, description ?? null, tech_stack ? JSON.stringify(tech_stack) : null, repo_path ?? null);
213
+ } catch (e) {
214
+ if (e.message?.includes("UNIQUE constraint failed")) {
215
+ return { content: [{ type: "text", text: `Project "${name}" already exists.` }], isError: true };
216
+ }
217
+ throw e;
218
+ }
219
+ return {
220
+ content: [{ type: "text", text: JSON.stringify({ project_id: id, message: `Project created: "${name}"` }) }]
221
+ };
222
+ }
223
+ );
224
+ server2.registerTool(
225
+ "list_projects",
226
+ {
227
+ title: "List Projects",
228
+ description: "List all tracked projects. Filter by status to see active, paused, completed, or archived projects.",
229
+ inputSchema: {
230
+ status: z.enum(["active", "paused", "completed", "archived"]).optional().describe("Filter by project status")
231
+ }
232
+ },
233
+ async ({ status }) => {
234
+ const db2 = getDb();
235
+ let rows;
236
+ if (status) {
237
+ rows = db2.prepare("SELECT * FROM projects WHERE status = ? ORDER BY updated_at DESC").all(status);
238
+ } else {
239
+ rows = db2.prepare("SELECT * FROM projects ORDER BY updated_at DESC").all();
240
+ }
241
+ return {
242
+ content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
243
+ };
244
+ }
245
+ );
246
+ server2.registerTool(
247
+ "get_project_status",
248
+ {
249
+ title: "Get Project Status",
250
+ description: "Get a full overview of a project: active tasks, recent decisions, blockers, and last session summary. Great for getting up to speed.",
251
+ inputSchema: {
252
+ project: z.string().describe("Project name or ID")
253
+ }
254
+ },
255
+ async ({ project }) => {
256
+ const db2 = getDb();
257
+ const projectId = resolveProjectId(project);
258
+ if (!projectId) {
259
+ return { content: [{ type: "text", text: `Project "${project}" not found.` }], isError: true };
260
+ }
261
+ const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(projectId);
262
+ const activeTasks = db2.prepare("SELECT id, title, status, priority, tags FROM tasks WHERE project_id = ? AND status NOT IN ('done', 'cancelled') ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END").all(projectId);
263
+ const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(projectId);
264
+ const recentDecisions = db2.prepare("SELECT id, title, decision, created_at FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT 5").all(projectId);
265
+ const lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(projectId);
266
+ const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(projectId);
267
+ const result = {
268
+ project: projectRow,
269
+ task_summary: taskCounts,
270
+ active_tasks: activeTasks,
271
+ blocked_tasks: blockedTasks,
272
+ recent_decisions: recentDecisions,
273
+ last_session: lastSession
274
+ };
275
+ return {
276
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
277
+ };
278
+ }
279
+ );
280
+ }
281
+
282
+ // src/tools/tasks.ts
283
+ import { z as z2 } from "zod/v4";
284
+ function registerTaskTools(server2) {
285
+ server2.registerTool(
286
+ "create_task",
287
+ {
288
+ title: "Create Task",
289
+ description: "Create a new task in a project. Proactively use this when the user mentions something that needs to be done, a bug to fix, or a feature to build.",
290
+ inputSchema: {
291
+ project: z2.string().optional().describe("Project name or ID (defaults to most recent active project)"),
292
+ title: z2.string().describe("Short task title"),
293
+ description: z2.string().optional().describe("Detailed description of the task"),
294
+ priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority (default: medium)"),
295
+ tags: z2.array(z2.string()).optional().describe('Tags like "backend", "auth", "bug"'),
296
+ parent_task_id: z2.string().optional().describe("Parent task ID for sub-tasks")
297
+ }
298
+ },
299
+ async ({ project, title, description, priority, tags, parent_task_id }) => {
300
+ const resolved = resolveProjectOrDefault(project);
301
+ if (!resolved) {
302
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
303
+ }
304
+ const db2 = getDb();
305
+ const id = generateId();
306
+ db2.prepare(
307
+ `INSERT INTO tasks (id, project_id, title, description, priority, tags, parent_task_id) VALUES (?, ?, ?, ?, ?, ?, ?)`
308
+ ).run(
309
+ id,
310
+ resolved.id,
311
+ title,
312
+ description ?? null,
313
+ priority ?? "medium",
314
+ tags ? JSON.stringify(tags) : null,
315
+ parent_task_id ?? null
316
+ );
317
+ return {
318
+ content: [{
319
+ type: "text",
320
+ text: JSON.stringify({
321
+ task_id: id,
322
+ message: `Task created: "${title}" in ${resolved.name} (priority: ${priority ?? "medium"})`
323
+ })
324
+ }]
325
+ };
326
+ }
327
+ );
328
+ server2.registerTool(
329
+ "update_task",
330
+ {
331
+ title: "Update Task",
332
+ description: "Update any field of a task. Proactively use this when a task status changes, priorities shift, or new information comes in.",
333
+ inputSchema: {
334
+ task_id: z2.string().describe("Task ID to update"),
335
+ title: z2.string().optional().describe("New title"),
336
+ description: z2.string().optional().describe("New description"),
337
+ status: z2.enum(["todo", "in_progress", "blocked", "done", "cancelled"]).optional().describe("New status"),
338
+ priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
339
+ tags: z2.array(z2.string()).optional().describe("New tags (replaces existing)"),
340
+ blocked_by: z2.array(z2.string()).optional().describe("Task IDs that block this task")
341
+ }
342
+ },
343
+ async ({ task_id, title, description, status, priority, tags, blocked_by }) => {
344
+ const db2 = getDb();
345
+ const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
346
+ if (!existing) {
347
+ return { content: [{ type: "text", text: `Task "${task_id}" not found.` }], isError: true };
348
+ }
349
+ const updates = [];
350
+ const params = [];
351
+ if (title !== void 0) {
352
+ updates.push("title = ?");
353
+ params.push(title);
354
+ }
355
+ if (description !== void 0) {
356
+ updates.push("description = ?");
357
+ params.push(description);
358
+ }
359
+ if (status !== void 0) {
360
+ updates.push("status = ?");
361
+ params.push(status);
362
+ if (status === "done") {
363
+ updates.push("completed_at = CURRENT_TIMESTAMP");
364
+ }
365
+ }
366
+ if (priority !== void 0) {
367
+ updates.push("priority = ?");
368
+ params.push(priority);
369
+ }
370
+ if (tags !== void 0) {
371
+ updates.push("tags = ?");
372
+ params.push(JSON.stringify(tags));
373
+ }
374
+ if (blocked_by !== void 0) {
375
+ updates.push("blocked_by = ?");
376
+ params.push(JSON.stringify(blocked_by));
377
+ if (blocked_by.length > 0 && status === void 0) {
378
+ updates.push("status = 'blocked'");
379
+ }
380
+ }
381
+ if (updates.length === 0) {
382
+ return { content: [{ type: "text", text: "No updates provided." }], isError: true };
383
+ }
384
+ params.push(task_id);
385
+ db2.prepare(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`).run(...params);
386
+ return {
387
+ content: [{ type: "text", text: JSON.stringify({ task_id, message: `Task "${existing.title}" updated.` }) }]
388
+ };
389
+ }
390
+ );
391
+ server2.registerTool(
392
+ "list_tasks",
393
+ {
394
+ title: "List Tasks",
395
+ description: "List tasks with filters. Defaults to showing non-completed tasks for the most recent active project.",
396
+ inputSchema: {
397
+ project: z2.string().optional().describe("Project name or ID"),
398
+ status: z2.enum(["todo", "in_progress", "blocked", "done", "cancelled"]).optional().describe("Filter by status"),
399
+ priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority"),
400
+ tag: z2.string().optional().describe("Filter by tag"),
401
+ include_done: z2.boolean().optional().describe("Include completed tasks (default: false)")
402
+ }
403
+ },
404
+ async ({ project, status, priority, tag, include_done }) => {
405
+ const resolved = resolveProjectOrDefault(project);
406
+ if (!resolved) {
407
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
408
+ }
409
+ const db2 = getDb();
410
+ const conditions = ["project_id = @projectId"];
411
+ const params = { projectId: resolved.id };
412
+ if (status) {
413
+ conditions.push("status = @status");
414
+ params.status = status;
415
+ } else if (!include_done) {
416
+ conditions.push("status NOT IN ('done', 'cancelled')");
417
+ }
418
+ if (priority) {
419
+ conditions.push("priority = @priority");
420
+ params.priority = priority;
421
+ }
422
+ if (tag) {
423
+ conditions.push("tags LIKE '%' || @tag || '%'");
424
+ params.tag = `"${tag}"`;
425
+ }
426
+ const sql = `SELECT * FROM tasks WHERE ${conditions.join(" AND ")} ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at DESC`;
427
+ const rows = db2.prepare(sql).all(params);
428
+ return {
429
+ content: [{ type: "text", text: JSON.stringify({ project: resolved.name, tasks: rows }, null, 2) }]
430
+ };
431
+ }
432
+ );
433
+ server2.registerTool(
434
+ "get_task",
435
+ {
436
+ title: "Get Task",
437
+ description: "Get full detail for a specific task including sub-tasks and related notes.",
438
+ inputSchema: {
439
+ task_id: z2.string().describe("Task ID")
440
+ }
441
+ },
442
+ async ({ task_id }) => {
443
+ const db2 = getDb();
444
+ const task = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
445
+ if (!task) {
446
+ return { content: [{ type: "text", text: `Task "${task_id}" not found.` }], isError: true };
447
+ }
448
+ const subtasks = db2.prepare("SELECT * FROM tasks WHERE parent_task_id = ?").all(task_id);
449
+ const notes = db2.prepare("SELECT * FROM notes WHERE task_id = ? ORDER BY created_at DESC").all(task_id);
450
+ return {
451
+ content: [{ type: "text", text: JSON.stringify({ task, subtasks, notes }, null, 2) }]
452
+ };
453
+ }
454
+ );
455
+ server2.registerTool(
456
+ "get_next_tasks",
457
+ {
458
+ title: "Get Next Tasks",
459
+ description: "Smart query: what should be worked on next? Returns highest priority non-blocked tasks for a project.",
460
+ inputSchema: {
461
+ project: z2.string().optional().describe("Project name or ID"),
462
+ limit: z2.number().optional().describe("Max number of tasks to return (default: 5)")
463
+ }
464
+ },
465
+ async ({ project, limit }) => {
466
+ const resolved = resolveProjectOrDefault(project);
467
+ if (!resolved) {
468
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
469
+ }
470
+ const db2 = getDb();
471
+ const rows = db2.prepare(
472
+ `SELECT * FROM tasks
473
+ WHERE project_id = ? AND status IN ('todo', 'in_progress')
474
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
475
+ created_at ASC
476
+ LIMIT ?`
477
+ ).all(resolved.id, limit ?? 5);
478
+ return {
479
+ content: [{
480
+ type: "text",
481
+ text: JSON.stringify({ project: resolved.name, next_tasks: rows }, null, 2)
482
+ }]
483
+ };
484
+ }
485
+ );
486
+ }
487
+
488
+ // src/tools/decisions.ts
489
+ import { z as z3 } from "zod/v4";
490
+ function registerDecisionTools(server2) {
491
+ server2.registerTool(
492
+ "log_decision",
493
+ {
494
+ title: "Log Decision",
495
+ description: "Record a decision with reasoning and alternatives considered. Proactively use this when the user makes a technical decision, chooses between options, or settles a debate.",
496
+ inputSchema: {
497
+ project: z3.string().optional().describe("Project name or ID"),
498
+ title: z3.string().describe("Short title for the decision"),
499
+ decision: z3.string().describe("What was decided"),
500
+ reasoning: z3.string().optional().describe("Why this was decided"),
501
+ alternatives: z3.array(z3.string()).optional().describe("Rejected alternatives"),
502
+ tags: z3.array(z3.string()).optional().describe('Tags like "architecture", "database", "api"')
503
+ }
504
+ },
505
+ async ({ project, title, decision, reasoning, alternatives, tags }) => {
506
+ const resolved = resolveProjectOrDefault(project);
507
+ if (!resolved) {
508
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
509
+ }
510
+ const db2 = getDb();
511
+ const id = generateId();
512
+ db2.prepare(
513
+ `INSERT INTO decisions (id, project_id, title, decision, reasoning, alternatives, tags) VALUES (?, ?, ?, ?, ?, ?, ?)`
514
+ ).run(
515
+ id,
516
+ resolved.id,
517
+ title,
518
+ decision,
519
+ reasoning ?? null,
520
+ alternatives ? JSON.stringify(alternatives) : null,
521
+ tags ? JSON.stringify(tags) : null
522
+ );
523
+ return {
524
+ content: [{
525
+ type: "text",
526
+ text: JSON.stringify({ decision_id: id, message: `Decision logged: "${title}" in ${resolved.name}` })
527
+ }]
528
+ };
529
+ }
530
+ );
531
+ server2.registerTool(
532
+ "list_decisions",
533
+ {
534
+ title: "List Decisions",
535
+ description: "List decisions for a project. Filter by tags to find specific decisions.",
536
+ inputSchema: {
537
+ project: z3.string().optional().describe("Project name or ID"),
538
+ tag: z3.string().optional().describe("Filter by tag"),
539
+ limit: z3.number().optional().describe("Max number of decisions to return (default: 20)")
540
+ }
541
+ },
542
+ async ({ project, tag, limit }) => {
543
+ const resolved = resolveProjectOrDefault(project);
544
+ if (!resolved) {
545
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
546
+ }
547
+ const db2 = getDb();
548
+ let sql;
549
+ const params = [resolved.id];
550
+ if (tag) {
551
+ sql = `SELECT * FROM decisions WHERE project_id = ? AND tags LIKE ? ORDER BY created_at DESC LIMIT ?`;
552
+ params.push(`%"${tag}"%`, limit ?? 20);
553
+ } else {
554
+ sql = `SELECT * FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT ?`;
555
+ params.push(limit ?? 20);
556
+ }
557
+ const rows = db2.prepare(sql).all(...params);
558
+ return {
559
+ content: [{ type: "text", text: JSON.stringify({ project: resolved.name, decisions: rows }, null, 2) }]
560
+ };
561
+ }
562
+ );
563
+ }
564
+
565
+ // src/tools/notes.ts
566
+ import { z as z4 } from "zod/v4";
567
+ function registerNoteTools(server2) {
568
+ server2.registerTool(
569
+ "add_note",
570
+ {
571
+ title: "Add Note",
572
+ description: "Add a note to a project or task. Proactively use this when the user shares context about architecture, bugs, ideas, research findings, or any important information worth remembering.",
573
+ inputSchema: {
574
+ project: z4.string().optional().describe("Project name or ID"),
575
+ content: z4.string().describe("The note content"),
576
+ category: z4.enum(["general", "architecture", "bug", "idea", "research", "meeting", "review"]).optional().describe("Note category (default: general)"),
577
+ task_id: z4.string().optional().describe("Link this note to a specific task"),
578
+ tags: z4.array(z4.string()).optional().describe("Tags for categorization")
579
+ }
580
+ },
581
+ async ({ project, content, category, task_id, tags }) => {
582
+ const resolved = resolveProjectOrDefault(project);
583
+ if (!resolved) {
584
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
585
+ }
586
+ const db2 = getDb();
587
+ const id = generateId();
588
+ db2.prepare(
589
+ `INSERT INTO notes (id, project_id, task_id, content, category, tags) VALUES (?, ?, ?, ?, ?, ?)`
590
+ ).run(
591
+ id,
592
+ resolved.id,
593
+ task_id ?? null,
594
+ content,
595
+ category ?? "general",
596
+ tags ? JSON.stringify(tags) : null
597
+ );
598
+ return {
599
+ content: [{
600
+ type: "text",
601
+ text: JSON.stringify({ note_id: id, message: `Note added to ${resolved.name} (${category ?? "general"})` })
602
+ }]
603
+ };
604
+ }
605
+ );
606
+ server2.registerTool(
607
+ "search_notes",
608
+ {
609
+ title: "Search Notes",
610
+ description: "Full-text search across notes for a project.",
611
+ inputSchema: {
612
+ project: z4.string().optional().describe("Project name or ID"),
613
+ query: z4.string().describe("Search query"),
614
+ category: z4.enum(["general", "architecture", "bug", "idea", "research", "meeting", "review"]).optional().describe("Filter by category")
615
+ }
616
+ },
617
+ async ({ project, query, category }) => {
618
+ const resolved = resolveProjectOrDefault(project);
619
+ if (!resolved) {
620
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
621
+ }
622
+ const db2 = getDb();
623
+ const conditions = ["project_id = ?"];
624
+ const params = [resolved.id];
625
+ conditions.push("content LIKE '%' || ? || '%'");
626
+ params.push(query);
627
+ if (category) {
628
+ conditions.push("category = ?");
629
+ params.push(category);
630
+ }
631
+ const sql = `SELECT * FROM notes WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC`;
632
+ const rows = db2.prepare(sql).all(...params);
633
+ return {
634
+ content: [{ type: "text", text: JSON.stringify({ project: resolved.name, results: rows }, null, 2) }]
635
+ };
636
+ }
637
+ );
638
+ server2.registerTool(
639
+ "set_context",
640
+ {
641
+ title: "Set Context",
642
+ description: "Set a key-value context pair for a project (upsert). Proactively use this when the user shares important project context like architecture decisions, config values, conventions, or constraints.",
643
+ inputSchema: {
644
+ project: z4.string().optional().describe("Project name or ID"),
645
+ key: z4.string().describe('Context key, e.g. "auth_approach", "deployment_target", "api_base_url"'),
646
+ value: z4.string().describe("Context value"),
647
+ category: z4.string().optional().describe('Category like "architecture", "config", "convention", "constraint"')
648
+ }
649
+ },
650
+ async ({ project, key, value, category }) => {
651
+ const resolved = resolveProjectOrDefault(project);
652
+ if (!resolved) {
653
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
654
+ }
655
+ const db2 = getDb();
656
+ const id = generateId();
657
+ db2.prepare(
658
+ `INSERT INTO context (id, project_id, key, value, category)
659
+ VALUES (?, ?, ?, ?, ?)
660
+ ON CONFLICT(project_id, key) DO UPDATE SET value = excluded.value, category = excluded.category`
661
+ ).run(id, resolved.id, key, value, category ?? "general");
662
+ return {
663
+ content: [{
664
+ type: "text",
665
+ text: JSON.stringify({ message: `Context set: "${key}" = "${value}" in ${resolved.name}` })
666
+ }]
667
+ };
668
+ }
669
+ );
670
+ server2.registerTool(
671
+ "get_context",
672
+ {
673
+ title: "Get Context",
674
+ description: "Get context by key or list all context for a project.",
675
+ inputSchema: {
676
+ project: z4.string().optional().describe("Project name or ID"),
677
+ key: z4.string().optional().describe("Specific context key to retrieve. If omitted, returns all context.")
678
+ }
679
+ },
680
+ async ({ project, key }) => {
681
+ const resolved = resolveProjectOrDefault(project);
682
+ if (!resolved) {
683
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
684
+ }
685
+ const db2 = getDb();
686
+ let rows;
687
+ if (key) {
688
+ rows = db2.prepare("SELECT * FROM context WHERE project_id = ? AND key = ?").all(resolved.id, key);
689
+ } else {
690
+ rows = db2.prepare("SELECT * FROM context WHERE project_id = ? ORDER BY category, key").all(resolved.id);
691
+ }
692
+ return {
693
+ content: [{ type: "text", text: JSON.stringify({ project: resolved.name, context: rows }, null, 2) }]
694
+ };
695
+ }
696
+ );
697
+ }
698
+
699
+ // src/tools/sessions.ts
700
+ import { z as z5 } from "zod/v4";
701
+ function registerSessionTools(server2) {
702
+ server2.registerTool(
703
+ "start_session",
704
+ {
705
+ title: "Start Session",
706
+ description: "Begin a work session for a project. Returns the full project overview including last session's next_steps, active tasks, blockers, and recent decisions. Call this at the start of every conversation.",
707
+ inputSchema: {
708
+ project: z5.string().optional().describe("Project name or ID")
709
+ }
710
+ },
711
+ async ({ project }) => {
712
+ const resolved = resolveProjectOrDefault(project);
713
+ if (!resolved) {
714
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
715
+ }
716
+ const db2 = getDb();
717
+ const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(resolved.id);
718
+ const lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(resolved.id);
719
+ const activeTasks = db2.prepare(
720
+ `SELECT id, title, status, priority, tags FROM tasks
721
+ WHERE project_id = ? AND status NOT IN ('done', 'cancelled')
722
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`
723
+ ).all(resolved.id);
724
+ const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
725
+ const recentDecisions = db2.prepare("SELECT id, title, decision, created_at FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT 5").all(resolved.id);
726
+ const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
727
+ const contextItems = db2.prepare("SELECT key, value, category FROM context WHERE project_id = ? ORDER BY category, key").all(resolved.id);
728
+ db2.prepare("UPDATE projects SET status = status WHERE id = ?").run(resolved.id);
729
+ const result = {
730
+ project: projectRow,
731
+ last_session: lastSession ? {
732
+ summary: lastSession.summary,
733
+ next_steps: lastSession.next_steps,
734
+ when: lastSession.created_at
735
+ } : null,
736
+ task_summary: taskCounts,
737
+ active_tasks: activeTasks,
738
+ blocked_tasks: blockedTasks,
739
+ recent_decisions: recentDecisions,
740
+ context: contextItems
741
+ };
742
+ return {
743
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
744
+ };
745
+ }
746
+ );
747
+ server2.registerTool(
748
+ "end_session",
749
+ {
750
+ title: "End Session",
751
+ description: "End a work session with a summary of what was accomplished and what to do next. Call this when the user is done working.",
752
+ inputSchema: {
753
+ project: z5.string().optional().describe("Project name or ID"),
754
+ summary: z5.string().describe("Summary of what was accomplished this session"),
755
+ tasks_worked_on: z5.array(z5.string()).optional().describe("Task IDs that were worked on"),
756
+ decisions_made: z5.array(z5.string()).optional().describe("Decision IDs that were made"),
757
+ next_steps: z5.string().optional().describe("What to do next time")
758
+ }
759
+ },
760
+ async ({ project, summary, tasks_worked_on, decisions_made, next_steps }) => {
761
+ const resolved = resolveProjectOrDefault(project);
762
+ if (!resolved) {
763
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
764
+ }
765
+ const db2 = getDb();
766
+ const id = generateId();
767
+ db2.prepare(
768
+ `INSERT INTO sessions (id, project_id, summary, tasks_worked_on, decisions_made, next_steps) VALUES (?, ?, ?, ?, ?, ?)`
769
+ ).run(
770
+ id,
771
+ resolved.id,
772
+ summary,
773
+ tasks_worked_on ? JSON.stringify(tasks_worked_on) : null,
774
+ decisions_made ? JSON.stringify(decisions_made) : null,
775
+ next_steps ?? null
776
+ );
777
+ return {
778
+ content: [{
779
+ type: "text",
780
+ text: JSON.stringify({ session_id: id, message: `Session ended for ${resolved.name}. Summary saved.` })
781
+ }]
782
+ };
783
+ }
784
+ );
785
+ }
786
+
787
+ // src/tools/queries.ts
788
+ import { z as z6 } from "zod/v4";
789
+ function registerQueryTools(server2) {
790
+ server2.registerTool(
791
+ "query",
792
+ {
793
+ title: "Query Database",
794
+ description: "Execute a read-only SQL query against the database. Only SELECT statements are allowed. Use this for custom queries not covered by other tools.",
795
+ inputSchema: {
796
+ sql: z6.string().describe("SQL SELECT query to execute")
797
+ }
798
+ },
799
+ async ({ sql }) => {
800
+ const trimmed = sql.trim();
801
+ if (!trimmed.toUpperCase().startsWith("SELECT")) {
802
+ return { content: [{ type: "text", text: "Only SELECT queries are allowed." }], isError: true };
803
+ }
804
+ const db2 = getDb();
805
+ try {
806
+ const stmt = db2.prepare(trimmed);
807
+ if (!stmt.reader) {
808
+ return { content: [{ type: "text", text: "Only read-only queries are allowed." }], isError: true };
809
+ }
810
+ const rows = stmt.all();
811
+ return {
812
+ content: [{ type: "text", text: JSON.stringify({ rows, count: rows.length }, null, 2) }]
813
+ };
814
+ } catch (e) {
815
+ return { content: [{ type: "text", text: `Query error: ${e.message}` }], isError: true };
816
+ }
817
+ }
818
+ );
819
+ server2.registerTool(
820
+ "get_project_summary",
821
+ {
822
+ title: "Get Project Summary",
823
+ description: "High-level summary of a project: total tasks by status, recent activity, open blockers, and upcoming priorities.",
824
+ inputSchema: {
825
+ project: z6.string().optional().describe("Project name or ID")
826
+ }
827
+ },
828
+ async ({ project }) => {
829
+ const resolved = resolveProjectOrDefault(project);
830
+ if (!resolved) {
831
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
832
+ }
833
+ const db2 = getDb();
834
+ const tasksByStatus = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
835
+ const blockers = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
836
+ const upcomingPriorities = db2.prepare(
837
+ `SELECT id, title, priority, status FROM tasks
838
+ WHERE project_id = ? AND status IN ('todo', 'in_progress')
839
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END
840
+ LIMIT 10`
841
+ ).all(resolved.id);
842
+ const recentActivity = db2.prepare(
843
+ `SELECT 'task' as type, title, updated_at FROM tasks WHERE project_id = ? AND updated_at > datetime('now', '-7 days')
844
+ UNION ALL
845
+ SELECT 'decision' as type, title, created_at as updated_at FROM decisions WHERE project_id = ? AND created_at > datetime('now', '-7 days')
846
+ UNION ALL
847
+ SELECT 'note' as type, substr(content, 1, 50) as title, created_at as updated_at FROM notes WHERE project_id = ? AND created_at > datetime('now', '-7 days')
848
+ ORDER BY updated_at DESC
849
+ LIMIT 20`
850
+ ).all(resolved.id, resolved.id, resolved.id);
851
+ const totalNotes = db2.prepare("SELECT COUNT(*) as count FROM notes WHERE project_id = ?").get(resolved.id);
852
+ const totalDecisions = db2.prepare("SELECT COUNT(*) as count FROM decisions WHERE project_id = ?").get(resolved.id);
853
+ const totalSessions = db2.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(resolved.id);
854
+ return {
855
+ content: [{
856
+ type: "text",
857
+ text: JSON.stringify(
858
+ {
859
+ project: resolved.name,
860
+ tasks_by_status: tasksByStatus,
861
+ blockers,
862
+ upcoming_priorities: upcomingPriorities,
863
+ recent_activity: recentActivity,
864
+ totals: { notes: totalNotes.count, decisions: totalDecisions.count, sessions: totalSessions.count }
865
+ },
866
+ null,
867
+ 2
868
+ )
869
+ }]
870
+ };
871
+ }
872
+ );
873
+ server2.registerTool(
874
+ "get_blockers",
875
+ {
876
+ title: "Get Blockers",
877
+ description: "List all blocked tasks with what's blocking them.",
878
+ inputSchema: {
879
+ project: z6.string().optional().describe("Project name or ID")
880
+ }
881
+ },
882
+ async ({ project }) => {
883
+ const resolved = resolveProjectOrDefault(project);
884
+ if (!resolved) {
885
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
886
+ }
887
+ const db2 = getDb();
888
+ const blockers = db2.prepare("SELECT * FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
889
+ const enriched = blockers.map((task) => {
890
+ let blockingTasks = [];
891
+ if (task.blocked_by) {
892
+ try {
893
+ const ids = JSON.parse(task.blocked_by);
894
+ blockingTasks = ids.map((id) => {
895
+ const blocking = db2.prepare("SELECT id, title, status FROM tasks WHERE id = ?").get(id);
896
+ return blocking ?? { id, title: "Unknown task", status: "unknown" };
897
+ });
898
+ } catch {
899
+ }
900
+ }
901
+ return { ...task, blocking_tasks: blockingTasks };
902
+ });
903
+ return {
904
+ content: [{ type: "text", text: JSON.stringify({ project: resolved.name, blockers: enriched }, null, 2) }]
905
+ };
906
+ }
907
+ );
908
+ server2.registerTool(
909
+ "search",
910
+ {
911
+ title: "Search Everything",
912
+ description: "Full-text search across tasks, notes, and decisions for a project.",
913
+ inputSchema: {
914
+ project: z6.string().optional().describe("Project name or ID"),
915
+ query: z6.string().describe("Search query")
916
+ }
917
+ },
918
+ async ({ project, query }) => {
919
+ const resolved = resolveProjectOrDefault(project);
920
+ if (!resolved) {
921
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
922
+ }
923
+ const db2 = getDb();
924
+ const pattern = `%${query}%`;
925
+ const tasks = db2.prepare("SELECT id, title, description, status, priority, 'task' as type FROM tasks WHERE project_id = ? AND (title LIKE ? OR description LIKE ?)").all(resolved.id, pattern, pattern);
926
+ const notes = db2.prepare("SELECT id, content, category, 'note' as type FROM notes WHERE project_id = ? AND content LIKE ?").all(resolved.id, pattern);
927
+ const decisions = db2.prepare("SELECT id, title, decision, reasoning, 'decision' as type FROM decisions WHERE project_id = ? AND (title LIKE ? OR decision LIKE ? OR reasoning LIKE ?)").all(resolved.id, pattern, pattern, pattern);
928
+ return {
929
+ content: [{
930
+ type: "text",
931
+ text: JSON.stringify(
932
+ {
933
+ project: resolved.name,
934
+ query,
935
+ results: { tasks, notes, decisions },
936
+ total: tasks.length + notes.length + decisions.length
937
+ },
938
+ null,
939
+ 2
940
+ )
941
+ }]
942
+ };
943
+ }
944
+ );
945
+ }
946
+
947
+ // src/index.ts
948
+ var server = new McpServer(
949
+ {
950
+ name: "mindpm",
951
+ version: "1.0.0"
952
+ },
953
+ {
954
+ capabilities: {
955
+ tools: {}
956
+ }
957
+ }
958
+ );
959
+ registerProjectTools(server);
960
+ registerTaskTools(server);
961
+ registerDecisionTools(server);
962
+ registerNoteTools(server);
963
+ registerSessionTools(server);
964
+ registerQueryTools(server);
965
+ async function main() {
966
+ const transport = new StdioServerTransport();
967
+ await server.connect(transport);
968
+ }
969
+ main().catch((error) => {
970
+ console.error("Fatal error:", error);
971
+ closeDb();
972
+ process.exit(1);
973
+ });
974
+ process.on("SIGINT", () => {
975
+ closeDb();
976
+ process.exit(0);
977
+ });
978
+ process.on("SIGTERM", () => {
979
+ closeDb();
980
+ process.exit(0);
981
+ });