@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/adapters/BackendApiAdapter.js +75 -0
- package/adapters/SqliteAdapter.js +82 -0
- package/adapters/SupabaseAdapter.js +76 -0
- package/auth.js +22 -0
- package/bin/init.js +165 -0
- package/index.js +437 -0
- package/lib/fileAnalyzer.js +220 -0
- package/package.json +59 -0
- package/supabase.js +4 -0
- package/tools/addMilestone.js +38 -0
- package/tools/addNoteToTask.js +35 -0
- package/tools/addPhase.js +34 -0
- package/tools/addSessionSummary.js +39 -0
- package/tools/addTask.js +52 -0
- package/tools/createProject.js +80 -0
- package/tools/deleteMilestone.js +39 -0
- package/tools/deletePhase.js +35 -0
- package/tools/deleteProject.js +35 -0
- package/tools/deleteTask.js +40 -0
- package/tools/editMilestone.js +39 -0
- package/tools/editPhase.js +34 -0
- package/tools/editTask.js +51 -0
- package/tools/exportToCloud.js +90 -0
- package/tools/getNextTasks.js +43 -0
- package/tools/getProjectRoadmap.js +27 -0
- package/tools/getProjectStatus.js +102 -0
- package/tools/getSessionHandoff.js +67 -0
- package/tools/getTasks.js +60 -0
- package/tools/renameProject.js +17 -0
- package/tools/scanRepo.js +215 -0
- package/tools/setProjectGoal.js +24 -0
- package/tools/syncProjects.js +99 -0
- package/tools/updateTaskStatus.js +47 -0
|
@@ -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();
|