@proplandev/mcp 1.0.4 → 1.0.5

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/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # ProPlan MCP
2
+
3
+ Persistent project memory and session continuity for Claude Code. Never lose context between sessions.
4
+
5
+ ## What it does
6
+
7
+ - **Remembers your project** — phases, milestones, tasks, and progress survive across sessions
8
+ - **Resumes automatically** — Claude picks up exactly where you left off
9
+ - **Scans your repo** — run `scan_repo` once and Claude understands your codebase
10
+ - **Works offline** — SQLite local mode, no account required
11
+ - **Syncs to dashboard** — optional cloud sync to [proplan.dev](https://project-planner-7zw4.onrender.com)
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ npx @proplandev/mcp init
17
+ ```
18
+
19
+ That's it. The wizard:
20
+ - Writes `.mcp.json` so Claude Code finds the server
21
+ - Writes `CLAUDE.md` with session resume instructions
22
+ - Updates `.gitignore` to protect local data
23
+ - Auto-adds read-only tools to Claude settings (skips approval prompts)
24
+
25
+ Then restart Claude Code, open your project, and type **start** or **continue**.
26
+
27
+ ## Tools
28
+
29
+ | Tool | What it does |
30
+ |---|---|
31
+ | `get_project_status` | List projects or get full status + session handoff |
32
+ | `create_project` | Create a new project with phases, milestones, tasks |
33
+ | `scan_repo` | Analyze codebase structure and auto-generate a roadmap |
34
+ | `get_project_roadmap` | Full roadmap JSON |
35
+ | `get_tasks` | Tasks filtered by status, milestone, or phase |
36
+ | `get_next_tasks` | What to work on next |
37
+ | `update_task_status` | Mark tasks pending / in_progress / completed |
38
+ | `add_note_to_task` | Attach notes to a task |
39
+ | `add_task` | Add a task to a milestone |
40
+ | `edit_task` | Edit task title or description |
41
+ | `delete_task` | Delete a task |
42
+ | `add_milestone` | Add a milestone to a phase |
43
+ | `edit_milestone` | Rename a milestone |
44
+ | `delete_milestone` | Delete a milestone and its tasks |
45
+ | `add_phase` | Add a phase to the roadmap |
46
+ | `edit_phase` | Rename a phase |
47
+ | `delete_phase` | Delete a phase and its milestones |
48
+ | `rename_project` | Rename a project |
49
+ | `delete_project` | Permanently delete a project |
50
+ | `set_project_goal` | Set the north-star goal for a project |
51
+ | `add_session_summary` | Record what was done this session |
52
+ | `export_to_cloud` | Sync local projects to the ProPlan dashboard |
53
+
54
+ ## Storage modes
55
+
56
+ **Local (default)** — data stored in `.project-planner/db.sqlite` in your project. No account needed.
57
+
58
+ **Cloud** — projects sync to [proplan.dev](https://project-planner-7zw4.onrender.com). Requires a free account and MCP token from Settings → Claude Code Integration.
59
+
60
+ Switch modes anytime by re-running `npx @proplandev/mcp init`.
61
+
62
+ ## Manual setup
63
+
64
+ If you prefer to configure `.mcp.json` yourself:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "project-planner": {
70
+ "command": "npx",
71
+ "args": ["-y", "@proplandev/mcp"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ For cloud mode, add your token:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "project-planner": {
83
+ "command": "npx",
84
+ "args": ["-y", "@proplandev/mcp"],
85
+ "env": {
86
+ "MCP_TOKEN": "your-token-here"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Requirements
94
+
95
+ - Node.js 18+
96
+ - Claude Code (or any MCP-compatible client)
package/bin/init.js CHANGED
@@ -68,6 +68,43 @@ function buildEntry(mode, mcpToken) {
68
68
  return entry;
69
69
  }
70
70
 
71
+ function addToGitignore(projectDir, entries) {
72
+ const gitignorePath = path.join(projectDir, '.gitignore');
73
+ let content = '';
74
+ try { content = fs.readFileSync(gitignorePath, 'utf8'); } catch { /* new file */ }
75
+ const lines = content.split('\n');
76
+ const toAdd = entries.filter(e => !lines.some(l => l.trim() === e));
77
+ if (toAdd.length === 0) return false;
78
+ const addition = (content.endsWith('\n') || content === '' ? '' : '\n') +
79
+ '# ProPlan MCP\n' + toAdd.join('\n') + '\n';
80
+ fs.writeFileSync(gitignorePath, content + addition, 'utf8');
81
+ return true;
82
+ }
83
+
84
+ const CLAUDE_MD_BLOCK = `
85
+ ## ProPlan MCP — Session Instructions
86
+
87
+ At the start of every session:
88
+ 1. Call \`get_project_status\` with \`include_handoff: true\` on the active project
89
+ 2. If projects exist: resume where things left off — say "I see we were working on X..."
90
+ 3. If no projects exist: call \`scan_repo\` to read the codebase, then ask "Want me to build a roadmap from this?"
91
+ 4. Never ask the user to re-explain context
92
+ `;
93
+
94
+ function writeClaudeMd(projectDir) {
95
+ const claudeMdPath = path.join(projectDir, 'CLAUDE.md');
96
+ const marker = 'ProPlan MCP — Session Instructions';
97
+ try {
98
+ const existing = fs.readFileSync(claudeMdPath, 'utf8');
99
+ if (existing.includes(marker)) return 'already';
100
+ fs.writeFileSync(claudeMdPath, existing.trimEnd() + '\n' + CLAUDE_MD_BLOCK, 'utf8');
101
+ return 'appended';
102
+ } catch {
103
+ fs.writeFileSync(claudeMdPath, CLAUDE_MD_BLOCK.trimStart(), 'utf8');
104
+ return 'created';
105
+ }
106
+ }
107
+
71
108
  // ─── Main ─────────────────────────────────────────────────────────────────────
72
109
 
73
110
  print();
@@ -106,9 +143,10 @@ print(` 2. User-level ${dim(userLevel)} ${dim('(applies to all your projec
106
143
  print();
107
144
  const locChoice = (await ask(' Your choice [1/2, default 1]: ')).trim() || '1';
108
145
  const mcpPath = locChoice === '2' ? userLevel : projectLocal;
146
+ const isProjectLocal = locChoice !== '2';
109
147
  print();
110
148
 
111
- // 3. Write
149
+ // 3. Write .mcp.json
112
150
  const existing = readMcpJson(mcpPath);
113
151
  existing.mcpServers = existing.mcpServers || {};
114
152
 
@@ -126,14 +164,31 @@ if (alreadyHas) {
126
164
 
127
165
  existing.mcpServers['project-planner'] = buildEntry(mode, mcpToken);
128
166
  writeMcpJson(mcpPath, existing);
129
-
130
- // 4. Success
131
167
  print(green(' ✓ .mcp.json written') + ' ' + dim(mcpPath));
168
+
169
+ // 4. CLAUDE.md — write or append session instructions
170
+ const claudeResult = writeClaudeMd(process.cwd());
171
+ if (claudeResult === 'created') {
172
+ print(green(' ✓ CLAUDE.md created') + ' ' + dim(path.join(process.cwd(), 'CLAUDE.md')));
173
+ } else if (claudeResult === 'appended') {
174
+ print(green(' ✓ CLAUDE.md updated') + ' ' + dim('ProPlan session instructions appended'));
175
+ } else {
176
+ print(dim(' ✓ CLAUDE.md already has ProPlan instructions — skipped'));
177
+ }
178
+
179
+ // 5. .gitignore — add relevant entries
180
+ const gitignoreEntries = ['.project-planner/'];
181
+ if (isProjectLocal && mode === 'cloud') gitignoreEntries.push('.mcp.json');
182
+ const gitignoreUpdated = addToGitignore(process.cwd(), gitignoreEntries);
183
+ if (gitignoreUpdated) {
184
+ print(green(' ✓ .gitignore updated') + ' ' + dim(gitignoreEntries.join(', ') + ' added'));
185
+ }
186
+
132
187
  print();
133
188
  print(bold(' Next steps'));
134
189
  print(` 1. Restart Claude Code (or reload the MCP server)`);
135
- print(` 2. Open a project directory and start a new session`);
136
- print(` 3. Claude will call get_project_status automatically on session start`);
190
+ print(` 2. Open your project directory in Claude Code`);
191
+ print(` 3. Type ${bold('start')} or ${bold('continue')} — Claude will scan your project and get to work`);
137
192
  print();
138
193
 
139
194
  if (mode === 'local') {
@@ -145,7 +200,7 @@ if (mode === 'local') {
145
200
  }
146
201
  print();
147
202
 
148
- // 5. allowedTools — offer to auto-write
203
+ // 6. allowedTools — offer to auto-write
149
204
  const ALLOWED_TOOLS = [
150
205
  'mcp__project-planner__get_project_status',
151
206
  'mcp__project-planner__get_next_tasks',
package/index.js CHANGED
@@ -50,7 +50,7 @@ console.error(`[ProPlan MCP] Starting in ${isCloudMode ? 'CLOUD' : 'LOCAL'} mode
50
50
  // ─── Server ───────────────────────────────────────────────────────────────────
51
51
  const server = new McpServer({
52
52
  name: 'project-planner',
53
- version: '1.0.0',
53
+ version: '1.0.5',
54
54
  });
55
55
 
56
56
  server.tool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proplandev/mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "MCP server for Claude Code — persistent project memory, session continuity, and structural repo analysis",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -22,8 +22,7 @@
22
22
  "adapters/",
23
23
  "tools/",
24
24
  "lib/",
25
- "auth.js",
26
- "supabase.js"
25
+ "README.md"
27
26
  ],
28
27
  "keywords": [
29
28
  "mcp",
@@ -45,7 +44,6 @@
45
44
  },
46
45
  "dependencies": {
47
46
  "@modelcontextprotocol/sdk": "^1.0.0",
48
- "@supabase/supabase-js": "^2.50.0",
49
47
  "better-sqlite3": "^12.8.0",
50
48
  "zod": "^3.23.0"
51
49
  },
@@ -32,7 +32,6 @@ export async function createProject(adapter, args) {
32
32
  title: taskInput.title,
33
33
  status: 'pending',
34
34
  order: taskOrder,
35
- resources: [],
36
35
  ...(taskInput.description && { description: cap(taskInput.description) }),
37
36
  ...(taskInput.technology && { technology: taskInput.technology }),
38
37
  };
@@ -28,9 +28,9 @@ export async function getNextTasks(adapter, args) {
28
28
  results.push({
29
29
  taskId: task.id,
30
30
  title: task.title,
31
- description: task.description ?? '',
31
+ ...(task.description && { description: task.description }),
32
32
  status: task.status,
33
- technology: task.technology ?? null,
33
+ ...(task.technology && { technology: task.technology }),
34
34
  phaseTitle: phase.title,
35
35
  milestoneTitle: milestone.title,
36
36
  });
@@ -1,76 +0,0 @@
1
- // mcp-server/adapters/SupabaseAdapter.js
2
-
3
- export class SupabaseAdapter {
4
- constructor(supabase, userId) {
5
- this._supabase = supabase;
6
- this._userId = userId;
7
- }
8
-
9
- getUserId() {
10
- return this._userId;
11
- }
12
-
13
- async listProjects() {
14
- const { data, error } = await this._supabase
15
- .from('roadmap')
16
- .select('id, title, content')
17
- .eq('user_id', this._userId);
18
- if (error) throw new Error(`Failed to fetch projects: ${error.message}`);
19
- return data || [];
20
- }
21
-
22
- async getProject(projectId) {
23
- const { data, error } = await this._supabase
24
- .from('roadmap')
25
- .select('id, title, content')
26
- .eq('user_id', this._userId)
27
- .eq('id', projectId)
28
- .single();
29
- if (error || !data) return null;
30
- return data;
31
- }
32
-
33
- async saveProject(projectId, title, content, updatedAt) {
34
- const { error } = await this._supabase
35
- .from('roadmap')
36
- .update({ title, content, updated_at: updatedAt })
37
- .eq('user_id', this._userId)
38
- .eq('id', projectId);
39
- if (error) throw new Error(`Failed to save: ${error.message}`);
40
- }
41
-
42
- async insertProject(title, content) {
43
- const now = new Date().toISOString();
44
- const { data, error } = await this._supabase
45
- .from('roadmap')
46
- .insert({
47
- user_id: this._userId,
48
- title,
49
- content,
50
- created_at: now,
51
- updated_at: now,
52
- })
53
- .select('id')
54
- .single();
55
- if (error || !data) throw new Error(`Failed to insert: ${error?.message ?? 'unknown'}`);
56
- return { id: data.id };
57
- }
58
-
59
- async deleteProject(projectId) {
60
- const { error } = await this._supabase
61
- .from('roadmap')
62
- .delete()
63
- .eq('user_id', this._userId)
64
- .eq('id', projectId);
65
- if (error) throw new Error(`Failed to delete project: ${error.message}`);
66
- }
67
-
68
- async renameProject(projectId, newTitle, updatedAt) {
69
- const { error } = await this._supabase
70
- .from('roadmap')
71
- .update({ title: newTitle, updated_at: updatedAt })
72
- .eq('user_id', this._userId)
73
- .eq('id', projectId);
74
- if (error) throw new Error(`Failed to rename project: ${error.message}`);
75
- }
76
- }
package/auth.js DELETED
@@ -1,22 +0,0 @@
1
- // mcp-server/auth.js
2
-
3
- /**
4
- * Validates a Personal Access Token against the mcp_tokens table.
5
- * @param {object} supabase - Supabase client
6
- * @param {string} token - The MCP_TOKEN value from env
7
- * @returns {Promise<string>} userId
8
- * @throws if token not found
9
- */
10
- export async function validatePat(supabase, token) {
11
- const { data, error } = await supabase
12
- .from('mcp_tokens')
13
- .select('user_id')
14
- .eq('token', token)
15
- .single();
16
-
17
- if (error || !data) {
18
- throw new Error('Invalid MCP_TOKEN. Generate a new one in Project Planner Settings.');
19
- }
20
-
21
- return data.user_id;
22
- }
package/supabase.js DELETED
@@ -1,4 +0,0 @@
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;
@@ -1,67 +0,0 @@
1
- // mcp-server/tools/getSessionHandoff.js
2
-
3
- export async function getSessionHandoff(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
- const limit = args.last_n_tasks ?? 5;
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
-
16
- // Flatten all tasks with their phase/milestone context
17
- const allTasks = [];
18
- for (const phase of (roadmap.phases || [])) {
19
- for (const milestone of (phase.milestones || [])) {
20
- for (const task of (milestone.tasks || [])) {
21
- const notes = Array.isArray(task.notes) ? task.notes : [];
22
- const lastNote = notes.length > 0 ? notes[notes.length - 1] : null;
23
- allTasks.push({
24
- id: task.id,
25
- title: task.title,
26
- status: task.status || 'pending',
27
- phase: phase.title,
28
- milestone: milestone.title,
29
- lastNote: lastNote ? { text: lastNote.text, createdAt: lastNote.createdAt } : null,
30
- _lastNoteTime: lastNote ? new Date(lastNote.createdAt).getTime() : 0,
31
- });
32
- }
33
- }
34
- }
35
-
36
- // Sort: in_progress first, then by most recent note descending
37
- allTasks.sort((a, b) => {
38
- if (a.status === 'in_progress' && b.status !== 'in_progress') return -1;
39
- if (b.status === 'in_progress' && a.status !== 'in_progress') return 1;
40
- return b._lastNoteTime - a._lastNoteTime;
41
- });
42
-
43
- const recentTasks = allTasks.slice(0, limit).map(({ _lastNoteTime, ...task }) => task);
44
-
45
- // Summarise overall progress
46
- const totalTasks = allTasks.length;
47
- const completedTasks = allTasks.filter(t => t.status === 'completed').length;
48
- const inProgressTasks = allTasks.filter(t => t.status === 'in_progress').length;
49
-
50
- // Last session summary (most recent only)
51
- const sessions = Array.isArray(roadmap.sessions) ? roadmap.sessions : [];
52
- const lastSession = sessions.length > 0 ? sessions[sessions.length - 1] : null;
53
-
54
- return {
55
- projectGoal: roadmap.projectGoal || null,
56
- project: {
57
- id: data.id,
58
- title: data.title,
59
- totalTasks,
60
- completedTasks,
61
- inProgressTasks,
62
- completionPercent: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0,
63
- },
64
- lastSession,
65
- recentTasks,
66
- };
67
- }
@@ -1,99 +0,0 @@
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
- }