@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,75 @@
1
+ // mcp-server/adapters/BackendApiAdapter.js
2
+ import { randomUUID } from 'crypto';
3
+
4
+ export class BackendApiAdapter {
5
+ constructor(token, apiUrl) {
6
+ this._token = token;
7
+ this._apiUrl = apiUrl;
8
+ }
9
+
10
+ getUserId() {
11
+ return 'cloud';
12
+ }
13
+
14
+ _headers() {
15
+ return {
16
+ 'Content-Type': 'application/json',
17
+ Authorization: `Bearer ${this._token}`,
18
+ };
19
+ }
20
+
21
+ async _request(method, path, body) {
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), 30_000);
24
+
25
+ try {
26
+ const res = await fetch(`${this._apiUrl}${path}`, {
27
+ method,
28
+ headers: this._headers(),
29
+ body: body ? JSON.stringify(body) : undefined,
30
+ signal: controller.signal,
31
+ });
32
+
33
+ if (!res.ok) {
34
+ if (res.status === 404) return null;
35
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
36
+ throw new Error(err.error || `Request failed: ${res.status}`);
37
+ }
38
+
39
+ return res.json();
40
+ } finally {
41
+ clearTimeout(timer);
42
+ }
43
+ }
44
+
45
+ async listProjects() {
46
+ return (await this._request('GET', '/api/mcp/projects')) ?? [];
47
+ }
48
+
49
+ async getProject(projectId) {
50
+ return this._request('GET', `/api/mcp/projects/${projectId}`);
51
+ }
52
+
53
+ async saveProject(projectId, title, content, _updatedAt) {
54
+ await this._request('PUT', `/api/mcp/projects/${projectId}`, { title, content });
55
+ }
56
+
57
+ async insertProject(title, content) {
58
+ const id = randomUUID();
59
+ const result = await this._request('POST', '/api/mcp/projects', { id, title, content });
60
+ // Use the id we sent so local and cloud UUIDs stay in sync
61
+ return { id: result?.id ?? id };
62
+ }
63
+
64
+ async deleteProject(projectId) {
65
+ await this._request('DELETE', `/api/mcp/projects/${projectId}`);
66
+ }
67
+
68
+ async renameProject(projectId, newTitle, _updatedAt) {
69
+ await this._request('PUT', `/api/mcp/projects/${projectId}`, { title: newTitle });
70
+ }
71
+
72
+ // These are local-only operations — no-ops in cloud mode
73
+ getProjectsSyncStatus() { return []; }
74
+ markSynced(_projectId, _syncedAt) {}
75
+ }
@@ -0,0 +1,82 @@
1
+ // mcp-server/adapters/SqliteAdapter.js
2
+ import Database from 'better-sqlite3';
3
+ import { mkdirSync } from 'fs';
4
+ import { dirname } from 'path';
5
+ import { randomUUID } from 'crypto';
6
+
7
+ const SCHEMA = `
8
+ CREATE TABLE IF NOT EXISTS projects (
9
+ id TEXT PRIMARY KEY,
10
+ user_id TEXT NOT NULL DEFAULT 'local',
11
+ title TEXT NOT NULL,
12
+ content TEXT NOT NULL,
13
+ created_at TEXT NOT NULL,
14
+ updated_at TEXT NOT NULL
15
+ );
16
+ `;
17
+
18
+ export class SqliteAdapter {
19
+ constructor(dbPath) {
20
+ mkdirSync(dirname(dbPath), { recursive: true });
21
+ this._db = new Database(dbPath);
22
+ this._db.exec(SCHEMA);
23
+ }
24
+
25
+ _applyMigrations() {
26
+ const cols = this._db.pragma('table_info(projects)');
27
+ if (!cols.some(c => c.name === 'last_synced_at')) {
28
+ this._db.exec('ALTER TABLE projects ADD COLUMN last_synced_at TEXT');
29
+ }
30
+ }
31
+
32
+ getUserId() {
33
+ return 'local';
34
+ }
35
+
36
+ listProjects() {
37
+ return this._db.prepare('SELECT id, title, content FROM projects').all();
38
+ }
39
+
40
+ getProject(projectId) {
41
+ return this._db
42
+ .prepare('SELECT id, title, content FROM projects WHERE id = ?')
43
+ .get(projectId) ?? null;
44
+ }
45
+
46
+ saveProject(projectId, title, content, updatedAt) {
47
+ this._db
48
+ .prepare('UPDATE projects SET title = ?, content = ?, updated_at = ? WHERE id = ?')
49
+ .run(title, content, updatedAt, projectId);
50
+ }
51
+
52
+ insertProject(title, content) {
53
+ const id = randomUUID();
54
+ const now = new Date().toISOString();
55
+ this._db
56
+ .prepare('INSERT INTO projects (id, user_id, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)')
57
+ .run(id, 'local', title, content, now, now);
58
+ return { id };
59
+ }
60
+
61
+ deleteProject(projectId) {
62
+ this._db.prepare('DELETE FROM projects WHERE id = ?').run(projectId);
63
+ }
64
+
65
+ renameProject(projectId, newTitle, updatedAt) {
66
+ this._db
67
+ .prepare('UPDATE projects SET title = ?, updated_at = ? WHERE id = ?')
68
+ .run(newTitle, updatedAt, projectId);
69
+ }
70
+
71
+ getProjectsSyncStatus() {
72
+ return this._db
73
+ .prepare('SELECT id, title, content, created_at, updated_at, last_synced_at FROM projects')
74
+ .all();
75
+ }
76
+
77
+ markSynced(projectId, syncedAt) {
78
+ this._db
79
+ .prepare('UPDATE projects SET last_synced_at = ? WHERE id = ?')
80
+ .run(syncedAt, projectId);
81
+ }
82
+ }
@@ -0,0 +1,76 @@
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 ADDED
@@ -0,0 +1,22 @@
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/bin/init.js ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ // mcp-server/bin/init.js
3
+ //
4
+ // ProPlan MCP init — writes .mcp.json for Claude Code (and compatible editors)
5
+ // so developers don't have to hand-edit JSON.
6
+ //
7
+ // Usage: node bin/init.js
8
+ // No dependencies beyond Node built-ins.
9
+
10
+ // ─── Node version guard ───────────────────────────────────────────────────────
11
+ const [major] = process.versions.node.split('.').map(Number);
12
+ if (major < 18) {
13
+ process.stderr.write(
14
+ `\n ProPlan init requires Node.js 18 or later.\n` +
15
+ ` You are running Node ${process.versions.node}.\n` +
16
+ ` Please upgrade: https://nodejs.org\n\n`
17
+ );
18
+ process.exit(1);
19
+ }
20
+
21
+ import readline from 'readline';
22
+ import fs from 'fs';
23
+ import path from 'path';
24
+ import os from 'os';
25
+ import { fileURLToPath } from 'url';
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+
29
+ const RESET = '\x1b[0m';
30
+ const BOLD = '\x1b[1m';
31
+ const GREEN = '\x1b[32m';
32
+ const CYAN = '\x1b[36m';
33
+ const YELLOW = '\x1b[33m';
34
+ const DIM = '\x1b[2m';
35
+
36
+ function print(msg = '') { process.stdout.write(msg + '\n'); }
37
+ function bold(s) { return BOLD + s + RESET; }
38
+ function green(s) { return GREEN + s + RESET; }
39
+ function cyan(s) { return CYAN + s + RESET; }
40
+ function dim(s) { return DIM + s + RESET; }
41
+
42
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
43
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
44
+
45
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
46
+
47
+ function readMcpJson(filePath) {
48
+ try {
49
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
50
+ } catch {
51
+ return { mcpServers: {} };
52
+ }
53
+ }
54
+
55
+ function writeMcpJson(filePath, data) {
56
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
57
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
58
+ }
59
+
60
+ function buildEntry(mode, mcpToken) {
61
+ const entry = {
62
+ command: 'npx',
63
+ args: ['-y', '@proplandev/mcp'],
64
+ };
65
+ if (mode === 'cloud' && mcpToken) {
66
+ entry.env = { MCP_TOKEN: mcpToken };
67
+ }
68
+ return entry;
69
+ }
70
+
71
+ // ─── Main ─────────────────────────────────────────────────────────────────────
72
+
73
+ print();
74
+ print(bold(' ProPlan MCP — Setup'));
75
+ print(dim(' Configures .mcp.json so Claude Code can find the server'));
76
+ print();
77
+
78
+ // 1. Mode
79
+ print(cyan(' Storage mode'));
80
+ print(' 1. Local — SQLite file in your project (.project-planner/db.sqlite)');
81
+ print(' 2. Cloud — Supabase + MCP token (required for web dashboard sync)');
82
+ print();
83
+ const modeChoice = (await ask(' Your choice [1/2, default 1]: ')).trim() || '1';
84
+ const mode = modeChoice === '2' ? 'cloud' : 'local';
85
+ print();
86
+
87
+ let mcpToken = '';
88
+ if (mode === 'cloud') {
89
+ print(cyan(' Cloud credentials'));
90
+ print(dim(' Generate your token at: https://project-planner-7zw4.onrender.com/settings'));
91
+ print();
92
+ mcpToken = (await ask(' MCP_TOKEN: ')).trim();
93
+ if (!mcpToken) {
94
+ print();
95
+ print(' ' + YELLOW + '⚠ No token entered — switching to local mode.' + RESET);
96
+ }
97
+ print();
98
+ }
99
+
100
+ // 2. Location
101
+ print(cyan(' Where to write .mcp.json'));
102
+ const projectLocal = path.join(process.cwd(), '.mcp.json');
103
+ const userLevel = path.join(os.homedir(), '.mcp.json');
104
+ print(` 1. Project-local ${dim(projectLocal)}`);
105
+ print(` 2. User-level ${dim(userLevel)} ${dim('(applies to all your projects)')}`);
106
+ print();
107
+ const locChoice = (await ask(' Your choice [1/2, default 1]: ')).trim() || '1';
108
+ const mcpPath = locChoice === '2' ? userLevel : projectLocal;
109
+ print();
110
+
111
+ // 3. Write
112
+ const existing = readMcpJson(mcpPath);
113
+ existing.mcpServers = existing.mcpServers || {};
114
+
115
+ const alreadyHas = existing.mcpServers['project-planner'];
116
+ if (alreadyHas) {
117
+ const overwrite = (await ask(` ${YELLOW}project-planner entry already exists. Overwrite? [y/N]: ${RESET}`)).trim().toLowerCase();
118
+ if (overwrite !== 'y') {
119
+ print();
120
+ print(' Aborted — existing entry preserved.');
121
+ rl.close();
122
+ process.exit(0);
123
+ }
124
+ print();
125
+ }
126
+
127
+ existing.mcpServers['project-planner'] = buildEntry(mode, mcpToken);
128
+ writeMcpJson(mcpPath, existing);
129
+
130
+ // 4. Success
131
+ print(green(' ✓ .mcp.json written') + ' ' + dim(mcpPath));
132
+ print();
133
+ print(bold(' Next steps'));
134
+ 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`);
137
+ print();
138
+
139
+ if (mode === 'local') {
140
+ print(dim(' Local mode: data stored in .project-planner/db.sqlite'));
141
+ print(dim(' Run export_to_cloud later to push your projects to the dashboard.'));
142
+ } else {
143
+ print(dim(' Cloud mode: projects sync automatically to your dashboard.'));
144
+ print(dim(' View them at https://project-planner-7zw4.onrender.com/dashboard'));
145
+ }
146
+ print();
147
+
148
+ // 5. allowedTools tip
149
+ print(bold(' Tip — skip approval prompts for read-only tools'));
150
+ print(dim(' Add this to ~/.claude/settings.json:'));
151
+ print();
152
+ print(dim(' {'));
153
+ print(dim(' "allowedTools": ['));
154
+ print(dim(' "mcp__project-planner__get_project_status",'));
155
+ print(dim(' "mcp__project-planner__get_next_tasks",'));
156
+ print(dim(' "mcp__project-planner__get_project_roadmap",'));
157
+ print(dim(' "mcp__project-planner__get_tasks",'));
158
+ print(dim(' "mcp__project-planner__add_session_summary",'));
159
+ print(dim(' "mcp__project-planner__update_task_status",'));
160
+ print(dim(' "mcp__project-planner__add_note_to_task"'));
161
+ print(dim(' ]'));
162
+ print(dim(' }'));
163
+ print();
164
+
165
+ rl.close();