@proplandev/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,99 @@
1
+ // mcp-server/tools/syncProjects.js
2
+
3
+ /**
4
+ * Core incremental sync algorithm, extracted for testability.
5
+ *
6
+ * @param {import('../adapters/SqliteAdapter.js').SqliteAdapter} adapter
7
+ * @param {object} supabase - Supabase client
8
+ * @param {string} userId
9
+ * @param {{ delete_removed?: boolean }} opts
10
+ * @returns {Promise<{ inserted: number, updated: number, skipped: number, deleted: number, failed: Array<{title: string, error: string}>, warning?: string }>}
11
+ */
12
+ export async function syncProjects(adapter, supabase, userId, opts = {}) {
13
+ const { delete_removed = false } = opts;
14
+
15
+ adapter._applyMigrations();
16
+
17
+ const projects = adapter.getProjectsSyncStatus();
18
+ const now = new Date().toISOString();
19
+
20
+ let inserted = 0;
21
+ let updated = 0;
22
+ let skipped = 0;
23
+ let deleted = 0;
24
+ const failed = [];
25
+
26
+ for (const project of projects) {
27
+ if (project.last_synced_at === null) {
28
+ // Never synced → INSERT with local UUID as Supabase row id
29
+ const { error } = await supabase
30
+ .from('roadmap')
31
+ .insert({
32
+ id: project.id,
33
+ user_id: userId,
34
+ title: project.title,
35
+ content: project.content,
36
+ created_at: now,
37
+ updated_at: now,
38
+ })
39
+ .select('id')
40
+ .single();
41
+
42
+ if (error) {
43
+ failed.push({ title: project.title, error: error.message });
44
+ } else {
45
+ adapter.markSynced(project.id, now);
46
+ inserted++;
47
+ }
48
+ } else if (project.updated_at > project.last_synced_at) {
49
+ // Changed since last sync → UPDATE
50
+ const { error } = await supabase
51
+ .from('roadmap')
52
+ .update({ title: project.title, content: project.content, updated_at: now })
53
+ .eq('id', project.id)
54
+ .eq('user_id', userId);
55
+
56
+ if (error) {
57
+ failed.push({ title: project.title, error: error.message });
58
+ } else {
59
+ adapter.markSynced(project.id, now);
60
+ updated++;
61
+ }
62
+ } else {
63
+ skipped++;
64
+ }
65
+ }
66
+
67
+ if (delete_removed) {
68
+ try {
69
+ const localIds = new Set(projects.map(p => p.id));
70
+ const { data: cloudRows, error: fetchErr } = await supabase
71
+ .from('roadmap')
72
+ .select('id')
73
+ .eq('user_id', userId);
74
+
75
+ if (fetchErr) throw fetchErr;
76
+
77
+ const toDelete = cloudRows.map(r => r.id).filter(id => !localIds.has(id));
78
+ if (toDelete.length > 0) {
79
+ const { error: delErr } = await supabase
80
+ .from('roadmap')
81
+ .delete()
82
+ .in('id', toDelete);
83
+ if (delErr) throw delErr;
84
+ deleted = toDelete.length;
85
+ }
86
+ } catch (err) {
87
+ return {
88
+ inserted,
89
+ updated,
90
+ skipped,
91
+ deleted: 0,
92
+ failed,
93
+ warning: `Cloud cleanup failed: ${err.message}. Inserts/updates were committed.`,
94
+ };
95
+ }
96
+ }
97
+
98
+ return { inserted, updated, skipped, deleted, failed };
99
+ }
@@ -0,0 +1,47 @@
1
+ // mcp-server/tools/updateTaskStatus.js
2
+
3
+ const VALID_STATUSES = ['pending', 'in_progress', 'completed'];
4
+ const NOTE_MAX_CHARS = 150;
5
+
6
+ export async function updateTaskStatus(adapter, args) {
7
+ if (!VALID_STATUSES.includes(args.status)) {
8
+ throw new Error(`Invalid status "${args.status}". Must be one of: ${VALID_STATUSES.join(', ')}`);
9
+ }
10
+
11
+ const data = await adapter.getProject(args.project_id);
12
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
13
+
14
+ let roadmap;
15
+ try {
16
+ roadmap = JSON.parse(data.content);
17
+ } catch {
18
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
19
+ }
20
+ let targetTask = null;
21
+
22
+ for (const phase of (roadmap.phases || [])) {
23
+ for (const milestone of (phase.milestones || [])) {
24
+ const task = (milestone.tasks || []).find(t => t.id === args.task_id);
25
+ if (task) {
26
+ task.status = args.status;
27
+
28
+ // Append note transactionally if provided (single DB write)
29
+ if (args.note && args.note.trim()) {
30
+ if (!Array.isArray(task.notes)) task.notes = [];
31
+ const noteText = args.note.trim().slice(0, NOTE_MAX_CHARS);
32
+ task.notes.push({ text: noteText, createdAt: new Date().toISOString() });
33
+ }
34
+
35
+ targetTask = task;
36
+ break;
37
+ }
38
+ }
39
+ if (targetTask) break;
40
+ }
41
+
42
+ if (!targetTask) throw new Error(`Task ${args.task_id} not found in project ${args.project_id}`);
43
+
44
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
45
+
46
+ return targetTask;
47
+ }