@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.
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@proplandev/mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Claude Code — persistent project memory, session continuity, and structural repo analysis",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "bin": {
11
+ "proplan-init": "bin/init.js",
12
+ "proplan-mcp": "index.js"
13
+ },
14
+ "scripts": {
15
+ "start": "node index.js",
16
+ "init": "node bin/init.js",
17
+ "test": "node --experimental-vm-modules node_modules/.bin/jest --testPathPatterns=tests/"
18
+ },
19
+ "files": [
20
+ "index.js",
21
+ "bin/",
22
+ "adapters/",
23
+ "tools/",
24
+ "lib/",
25
+ "auth.js",
26
+ "supabase.js"
27
+ ],
28
+ "keywords": [
29
+ "mcp",
30
+ "claude",
31
+ "claude-code",
32
+ "project-planner",
33
+ "session-memory",
34
+ "ai-tools"
35
+ ],
36
+ "author": "Solomon Agyire",
37
+ "license": "MIT",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/King-Proplan/project-planner.git",
44
+ "directory": "mcp-server"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.0.0",
48
+ "@supabase/supabase-js": "^2.50.0",
49
+ "better-sqlite3": "^12.8.0",
50
+ "zod": "^3.23.0"
51
+ },
52
+ "devDependencies": {
53
+ "jest": "^30.3.0"
54
+ },
55
+ "jest": {
56
+ "testEnvironment": "node",
57
+ "transform": {}
58
+ }
59
+ }
package/supabase.js ADDED
@@ -0,0 +1,4 @@
1
+ // mcp-server/supabase.js
2
+ // Supabase direct access removed. Cloud mode now routes through the backend API.
3
+ // See BackendApiAdapter.js.
4
+ export const supabase = null;
@@ -0,0 +1,38 @@
1
+ // mcp-server/tools/addMilestone.js
2
+
3
+ export async function addMilestone(adapter, args) {
4
+ const data = await adapter.getProject(args.project_id);
5
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
6
+
7
+ let roadmap;
8
+ try {
9
+ roadmap = JSON.parse(data.content);
10
+ } catch {
11
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
12
+ }
13
+
14
+ const phase = (roadmap.phases || []).find(p => p.id === args.phase_id);
15
+ if (!phase) throw new Error(`Phase ${args.phase_id} not found in project ${args.project_id}`);
16
+
17
+ if (args.dry_run) {
18
+ return {
19
+ preview: { id: '(will be generated)', title: args.title, tasks: [] },
20
+ target: `phase '${phase.title}'`,
21
+ action: 'add_milestone',
22
+ };
23
+ }
24
+
25
+ const milestones = phase.milestones || [];
26
+ const maxOrder = milestones.reduce((max, m) => Math.max(max, m.order || 0), 0);
27
+ const newMilestone = {
28
+ id: `milestone-${Date.now()}`,
29
+ title: args.title,
30
+ order: maxOrder + 1,
31
+ tasks: [],
32
+ };
33
+ phase.milestones = [...milestones, newMilestone];
34
+
35
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
36
+
37
+ return newMilestone;
38
+ }
@@ -0,0 +1,35 @@
1
+ // mcp-server/tools/addNoteToTask.js
2
+
3
+ export async function addNoteToTask(adapter, args) {
4
+ if (!args.note || !args.note.trim()) throw new Error('Note text is required');
5
+
6
+ const data = await adapter.getProject(args.project_id);
7
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
8
+
9
+ let roadmap;
10
+ try {
11
+ roadmap = JSON.parse(data.content);
12
+ } catch {
13
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
14
+ }
15
+ let targetTask = null;
16
+
17
+ for (const phase of (roadmap.phases || [])) {
18
+ for (const milestone of (phase.milestones || [])) {
19
+ const task = (milestone.tasks || []).find(t => t.id === args.task_id);
20
+ if (task) {
21
+ if (!Array.isArray(task.notes)) task.notes = [];
22
+ task.notes.push({ text: args.note.trim(), createdAt: new Date().toISOString() });
23
+ targetTask = task;
24
+ break;
25
+ }
26
+ }
27
+ if (targetTask) break;
28
+ }
29
+
30
+ if (!targetTask) throw new Error(`Task ${args.task_id} not found in project ${args.project_id}`);
31
+
32
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
33
+
34
+ return targetTask;
35
+ }
@@ -0,0 +1,34 @@
1
+ // mcp-server/tools/addPhase.js
2
+
3
+ export async function addPhase(adapter, args) {
4
+ const data = await adapter.getProject(args.project_id);
5
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
6
+
7
+ let roadmap;
8
+ try {
9
+ roadmap = JSON.parse(data.content);
10
+ } catch {
11
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
12
+ }
13
+
14
+ if (args.dry_run) {
15
+ return {
16
+ preview: { id: '(will be generated)', title: args.title, milestones: [] },
17
+ action: 'add_phase',
18
+ };
19
+ }
20
+
21
+ const phases = roadmap.phases || [];
22
+ const maxOrder = phases.reduce((max, p) => Math.max(max, p.order || 0), 0);
23
+ const newPhase = {
24
+ id: `phase-${Date.now()}`,
25
+ title: args.title,
26
+ order: maxOrder + 1,
27
+ milestones: [],
28
+ };
29
+ roadmap.phases = [...phases, newPhase];
30
+
31
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
32
+
33
+ return newPhase;
34
+ }
@@ -0,0 +1,39 @@
1
+ // mcp-server/tools/addSessionSummary.js
2
+
3
+ const MAX_SESSIONS = 10;
4
+
5
+ export async function addSessionSummary(adapter, args) {
6
+ if (!args.summary || !args.summary.trim()) {
7
+ throw new Error('summary is required and cannot be empty');
8
+ }
9
+
10
+ const data = await adapter.getProject(args.project_id);
11
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
12
+
13
+ let roadmap;
14
+ try {
15
+ roadmap = JSON.parse(data.content);
16
+ } catch {
17
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
18
+ }
19
+
20
+ if (!Array.isArray(roadmap.sessions)) roadmap.sessions = [];
21
+
22
+ roadmap.sessions.push({
23
+ summary: args.summary.trim(),
24
+ createdAt: new Date().toISOString(),
25
+ });
26
+
27
+ // Keep only the most recent MAX_SESSIONS — drop oldest
28
+ if (roadmap.sessions.length > MAX_SESSIONS) {
29
+ roadmap.sessions = roadmap.sessions.slice(-MAX_SESSIONS);
30
+ }
31
+
32
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
33
+
34
+ return {
35
+ saved: true,
36
+ totalSessions: roadmap.sessions.length,
37
+ summary: roadmap.sessions[roadmap.sessions.length - 1],
38
+ };
39
+ }
@@ -0,0 +1,52 @@
1
+ // mcp-server/tools/addTask.js
2
+
3
+ const DESC_MAX = 300;
4
+ const cap = s => s ? s.slice(0, DESC_MAX) : s;
5
+
6
+ export async function addTask(adapter, args) {
7
+ const data = await adapter.getProject(args.project_id);
8
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
9
+
10
+ let roadmap;
11
+ try {
12
+ roadmap = JSON.parse(data.content);
13
+ } catch {
14
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
15
+ }
16
+
17
+ const phase = (roadmap.phases || []).find(p => p.id === args.phase_id);
18
+ if (!phase) throw new Error(`Phase ${args.phase_id} not found in project ${args.project_id}`);
19
+
20
+ const milestone = (phase.milestones || []).find(m => m.id === args.milestone_id);
21
+ if (!milestone) throw new Error(`Milestone ${args.milestone_id} not found in project ${args.project_id}`);
22
+
23
+ if (args.dry_run) {
24
+ return {
25
+ preview: {
26
+ id: '(will be generated)',
27
+ title: args.title,
28
+ status: 'pending',
29
+ ...(args.description && { description: args.description }),
30
+ ...(args.technology && { technology: args.technology }),
31
+ },
32
+ target: `milestone '${milestone.title}' in phase '${phase.title}'`,
33
+ action: 'add_task',
34
+ };
35
+ }
36
+
37
+ const tasks = milestone.tasks || [];
38
+ const maxOrder = tasks.reduce((max, t) => Math.max(max, t.order || 0), 0);
39
+ const newTask = {
40
+ id: `task-${Date.now()}`,
41
+ title: args.title,
42
+ status: 'pending',
43
+ order: maxOrder + 1,
44
+ ...(args.description && { description: cap(args.description) }),
45
+ ...(args.technology && { technology: args.technology }),
46
+ };
47
+ milestone.tasks = [...tasks, newTask];
48
+
49
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
50
+
51
+ return newTask;
52
+ }
@@ -0,0 +1,80 @@
1
+ // mcp-server/tools/createProject.js
2
+
3
+ const DESC_MAX = 300;
4
+ const cap = s => s ? s.slice(0, DESC_MAX) : s;
5
+
6
+ export async function createProject(adapter, args) {
7
+ if (!args.phases || args.phases.length === 0) {
8
+ throw new Error('Project must have at least one phase');
9
+ }
10
+
11
+ const now = new Date().toISOString();
12
+
13
+ let idCounter = 0;
14
+ let phaseOrder = 0;
15
+ let milestoneCount = 0;
16
+ let taskCount = 0;
17
+
18
+ const phases = args.phases.map(phaseInput => {
19
+ phaseOrder += 1;
20
+ let milestoneOrder = 0;
21
+
22
+ const milestones = (phaseInput.milestones || []).map(msInput => {
23
+ milestoneOrder += 1;
24
+ milestoneCount += 1;
25
+ let taskOrder = 0;
26
+
27
+ const tasks = (msInput.tasks || []).map(taskInput => {
28
+ taskOrder += 1;
29
+ taskCount += 1;
30
+ return {
31
+ id: `task-${Date.now()}-${++idCounter}`,
32
+ title: taskInput.title,
33
+ status: 'pending',
34
+ order: taskOrder,
35
+ resources: [],
36
+ ...(taskInput.description && { description: cap(taskInput.description) }),
37
+ ...(taskInput.technology && { technology: taskInput.technology }),
38
+ };
39
+ });
40
+
41
+ return {
42
+ id: `milestone-${Date.now()}-${++idCounter}`,
43
+ title: msInput.title,
44
+ order: milestoneOrder,
45
+ tasks,
46
+ };
47
+ });
48
+
49
+ return {
50
+ id: `phase-${Date.now()}-${++idCounter}`,
51
+ title: phaseInput.title,
52
+ order: phaseOrder,
53
+ milestones,
54
+ };
55
+ });
56
+
57
+ const roadmap = {
58
+ projectName: args.title,
59
+ summary: '',
60
+ metadata: {
61
+ scope: '',
62
+ version: '1.0',
63
+ ...(args.description && { description: args.description }),
64
+ ...(args.timeline && { timeline: args.timeline }),
65
+ ...(args.experienceLevel && { experienceLevel: args.experienceLevel }),
66
+ ...(args.technologies && { technologies: args.technologies }),
67
+ },
68
+ phases,
69
+ };
70
+
71
+ const { id } = await adapter.insertProject(args.title, JSON.stringify(roadmap));
72
+
73
+ return {
74
+ projectId: id,
75
+ title: args.title,
76
+ phaseCount: phases.length,
77
+ milestoneCount,
78
+ taskCount,
79
+ };
80
+ }
@@ -0,0 +1,39 @@
1
+ // mcp-server/tools/deleteMilestone.js
2
+
3
+ export async function deleteMilestone(adapter, args) {
4
+ const data = await adapter.getProject(args.project_id);
5
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
6
+
7
+ let roadmap;
8
+ try {
9
+ roadmap = JSON.parse(data.content);
10
+ } catch {
11
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
12
+ }
13
+
14
+ let targetMilestone = null;
15
+ let targetPhase = null;
16
+
17
+ for (const phase of (roadmap.phases || [])) {
18
+ const ms = (phase.milestones || []).find(m => m.id === args.milestone_id);
19
+ if (ms) { targetMilestone = ms; targetPhase = phase; break; }
20
+ }
21
+
22
+ if (!targetMilestone) throw new Error(`Milestone ${args.milestone_id} not found in project ${args.project_id}`);
23
+
24
+ const taskCount = (targetMilestone.tasks || []).length;
25
+
26
+ if (args.dry_run) {
27
+ return {
28
+ item: { id: targetMilestone.id, title: targetMilestone.title },
29
+ warning: `This will permanently delete 1 milestone and ${taskCount} task${taskCount !== 1 ? 's' : ''}. This cannot be undone.`,
30
+ action: 'delete_milestone',
31
+ };
32
+ }
33
+
34
+ targetPhase.milestones = targetPhase.milestones.filter(m => m.id !== args.milestone_id);
35
+
36
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
37
+
38
+ return { deleted: true, id: args.milestone_id };
39
+ }
@@ -0,0 +1,35 @@
1
+ // mcp-server/tools/deletePhase.js
2
+
3
+ export async function deletePhase(adapter, args) {
4
+ const data = await adapter.getProject(args.project_id);
5
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
6
+
7
+ let roadmap;
8
+ try {
9
+ roadmap = JSON.parse(data.content);
10
+ } catch {
11
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
12
+ }
13
+
14
+ const targetPhase = (roadmap.phases || []).find(p => p.id === args.phase_id);
15
+ if (!targetPhase) throw new Error(`Phase ${args.phase_id} not found in project ${args.project_id}`);
16
+
17
+ const milestoneCount = (targetPhase.milestones || []).length;
18
+ const taskCount = (targetPhase.milestones || []).reduce(
19
+ (sum, m) => sum + (m.tasks || []).length, 0
20
+ );
21
+
22
+ if (args.dry_run) {
23
+ return {
24
+ item: { id: targetPhase.id, title: targetPhase.title },
25
+ warning: `This will permanently delete 1 phase, ${milestoneCount} milestone${milestoneCount !== 1 ? 's' : ''}, and ${taskCount} task${taskCount !== 1 ? 's' : ''}. This cannot be undone.`,
26
+ action: 'delete_phase',
27
+ };
28
+ }
29
+
30
+ roadmap.phases = roadmap.phases.filter(p => p.id !== args.phase_id);
31
+
32
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
33
+
34
+ return { deleted: true, id: args.phase_id };
35
+ }
@@ -0,0 +1,35 @@
1
+ // mcp-server/tools/deleteProject.js
2
+
3
+ export async function deleteProject(adapter, args) {
4
+ const data = await adapter.getProject(args.project_id);
5
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
6
+
7
+ let taskCount = 0;
8
+ let phaseCount = 0;
9
+ let milestoneCount = 0;
10
+
11
+ try {
12
+ const roadmap = JSON.parse(data.content);
13
+ phaseCount = (roadmap.phases || []).length;
14
+ for (const phase of (roadmap.phases || [])) {
15
+ milestoneCount += (phase.milestones || []).length;
16
+ for (const milestone of (phase.milestones || [])) {
17
+ taskCount += (milestone.tasks || []).length;
18
+ }
19
+ }
20
+ } catch {
21
+ // corrupted content — still allow deletion
22
+ }
23
+
24
+ if (args.dry_run) {
25
+ return {
26
+ item: { id: data.id, title: data.title },
27
+ warning: `This will permanently delete the project "${data.title}" including ${phaseCount} phase${phaseCount !== 1 ? 's' : ''}, ${milestoneCount} milestone${milestoneCount !== 1 ? 's' : ''}, and ${taskCount} task${taskCount !== 1 ? 's' : ''}. This cannot be undone.`,
28
+ action: 'delete_project',
29
+ };
30
+ }
31
+
32
+ await adapter.deleteProject(args.project_id);
33
+
34
+ return { deleted: true, id: args.project_id, title: data.title };
35
+ }
@@ -0,0 +1,40 @@
1
+ // mcp-server/tools/deleteTask.js
2
+
3
+ export async function deleteTask(adapter, args) {
4
+ const data = await adapter.getProject(args.project_id);
5
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
6
+
7
+ let roadmap;
8
+ try {
9
+ roadmap = JSON.parse(data.content);
10
+ } catch {
11
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
12
+ }
13
+
14
+ let targetTask = null;
15
+ let targetMilestone = null;
16
+
17
+ for (const phase of (roadmap.phases || [])) {
18
+ for (const milestone of (phase.milestones || [])) {
19
+ const task = (milestone.tasks || []).find(t => t.id === args.task_id);
20
+ if (task) { targetTask = task; targetMilestone = milestone; break; }
21
+ }
22
+ if (targetTask) break;
23
+ }
24
+
25
+ if (!targetTask) throw new Error(`Task ${args.task_id} not found in project ${args.project_id}`);
26
+
27
+ if (args.dry_run) {
28
+ return {
29
+ item: { id: targetTask.id, title: targetTask.title, status: targetTask.status },
30
+ warning: 'This will permanently delete 1 task. This cannot be undone.',
31
+ action: 'delete_task',
32
+ };
33
+ }
34
+
35
+ targetMilestone.tasks = targetMilestone.tasks.filter(t => t.id !== args.task_id);
36
+
37
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
38
+
39
+ return { deleted: true, id: args.task_id };
40
+ }
@@ -0,0 +1,39 @@
1
+ // mcp-server/tools/editMilestone.js
2
+
3
+ export async function editMilestone(adapter, args) {
4
+ if (args.title === undefined) {
5
+ throw new Error('Provide at least one field to update: title');
6
+ }
7
+
8
+ const data = await adapter.getProject(args.project_id);
9
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
10
+
11
+ let roadmap;
12
+ try {
13
+ roadmap = JSON.parse(data.content);
14
+ } catch {
15
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
16
+ }
17
+
18
+ let targetMilestone = null;
19
+ for (const phase of (roadmap.phases || [])) {
20
+ const ms = (phase.milestones || []).find(m => m.id === args.milestone_id);
21
+ if (ms) { targetMilestone = ms; break; }
22
+ }
23
+
24
+ if (!targetMilestone) throw new Error(`Milestone ${args.milestone_id} not found in project ${args.project_id}`);
25
+
26
+ if (args.dry_run) {
27
+ return {
28
+ before: { title: targetMilestone.title },
29
+ after: { title: args.title },
30
+ action: 'edit_milestone',
31
+ };
32
+ }
33
+
34
+ targetMilestone.title = args.title;
35
+
36
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
37
+
38
+ return targetMilestone;
39
+ }
@@ -0,0 +1,34 @@
1
+ // mcp-server/tools/editPhase.js
2
+
3
+ export async function editPhase(adapter, args) {
4
+ if (args.title === undefined) {
5
+ throw new Error('Provide at least one field to update: title');
6
+ }
7
+
8
+ const data = await adapter.getProject(args.project_id);
9
+ if (!data) throw new Error(`Project ${args.project_id} not found`);
10
+
11
+ let roadmap;
12
+ try {
13
+ roadmap = JSON.parse(data.content);
14
+ } catch {
15
+ throw new Error(`Project ${data.id} has corrupted roadmap data`);
16
+ }
17
+
18
+ const targetPhase = (roadmap.phases || []).find(p => p.id === args.phase_id);
19
+ if (!targetPhase) throw new Error(`Phase ${args.phase_id} not found in project ${args.project_id}`);
20
+
21
+ if (args.dry_run) {
22
+ return {
23
+ before: { title: targetPhase.title },
24
+ after: { title: args.title },
25
+ action: 'edit_phase',
26
+ };
27
+ }
28
+
29
+ targetPhase.title = args.title;
30
+
31
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
32
+
33
+ return targetPhase;
34
+ }
@@ -0,0 +1,51 @@
1
+ // mcp-server/tools/editTask.js
2
+
3
+ const DESC_MAX = 300;
4
+
5
+ export async function editTask(adapter, args) {
6
+ const hasFields = args.title !== undefined || args.description !== undefined || args.technology !== undefined;
7
+ if (!hasFields) {
8
+ throw new Error('Provide at least one field to update: title, description, technology');
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
+
21
+ let targetTask = null;
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) { targetTask = task; break; }
26
+ }
27
+ if (targetTask) break;
28
+ }
29
+
30
+ if (!targetTask) throw new Error(`Task ${args.task_id} not found in project ${args.project_id}`);
31
+
32
+ if (args.dry_run) {
33
+ return {
34
+ before: { title: targetTask.title, description: targetTask.description, technology: targetTask.technology },
35
+ after: {
36
+ title: args.title !== undefined ? args.title : targetTask.title,
37
+ description: args.description !== undefined ? args.description : targetTask.description,
38
+ technology: args.technology !== undefined ? args.technology : targetTask.technology,
39
+ },
40
+ action: 'edit_task',
41
+ };
42
+ }
43
+
44
+ if (args.title !== undefined) targetTask.title = args.title;
45
+ if (args.description !== undefined) targetTask.description = args.description.slice(0, DESC_MAX);
46
+ if (args.technology !== undefined) targetTask.technology = args.technology;
47
+
48
+ await adapter.saveProject(args.project_id, data.title, JSON.stringify(roadmap), new Date().toISOString());
49
+
50
+ return targetTask;
51
+ }