projoflow-mcp-server 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.
Files changed (3) hide show
  1. package/README.md +169 -0
  2. package/index.js +641 -0
  3. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # ProjoFlow MCP Server
2
+
3
+ Connect your AI assistant (Claude Code, Cursor, Windsurf, etc.) directly to your ProjoFlow project management instance.
4
+
5
+ **The only project management tool your AI can talk to.™**
6
+
7
+ ## Features
8
+
9
+ 18 powerful tools for AI-powered project management:
10
+
11
+ | Category | Tools |
12
+ |----------|-------|
13
+ | **Projects** | `list_projects`, `get_project`, `create_project`, `update_project`, `get_project_summary` |
14
+ | **Tasks** | `list_tasks`, `create_task`, `update_task` |
15
+ | **Time Tracking** | `log_time`, `get_time_entries` |
16
+ | **Clients** | `list_clients`, `create_client` |
17
+ | **Leads** | `list_leads`, `update_lead` |
18
+ | **Notes** | `add_note` |
19
+ | **Comments** | `list_task_comments`, `add_task_comment` |
20
+ | **Dashboard** | `get_dashboard` |
21
+
22
+ ## Quick Start
23
+
24
+ ### 1. Create an MCP User in ProjoFlow
25
+
26
+ In your ProjoFlow instance, create a user account for the MCP server:
27
+ - Email: `mcp@your-domain.com` (or any email)
28
+ - Password: Generate a secure password
29
+ - Role: Admin (for full access) or restricted as needed
30
+
31
+ ### 2. Get Your Supabase Credentials
32
+
33
+ From your Supabase project dashboard:
34
+ - **Project URL**: `https://your-project-id.supabase.co`
35
+ - **Anon Key**: Found in Settings → API → Project API keys
36
+
37
+ ### 3. Configure Claude Code Desktop
38
+
39
+ Add to your `claude_desktop_config.json`:
40
+
41
+ **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
42
+ **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
43
+ **Linux**: `~/.config/Claude/claude_desktop_config.json`
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "projoflow": {
49
+ "command": "node",
50
+ "args": ["/path/to/projoflow/mcp-server/index.js"],
51
+ "env": {
52
+ "PROJOFLOW_SUPABASE_URL": "https://your-project.supabase.co",
53
+ "PROJOFLOW_SUPABASE_ANON_KEY": "your-anon-key",
54
+ "PROJOFLOW_MCP_EMAIL": "mcp@your-domain.com",
55
+ "PROJOFLOW_MCP_PASSWORD": "your-secure-password"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### 4. Restart Claude Code
63
+
64
+ Restart Claude Code Desktop to load the MCP server.
65
+
66
+ ## Usage Examples
67
+
68
+ Once connected, your AI assistant can:
69
+
70
+ **List active projects:**
71
+ > "Show me all active projects"
72
+
73
+ **Create a task:**
74
+ > "Create a task 'Fix login bug' in the Website Redesign project with high priority"
75
+
76
+ **Log time:**
77
+ > "Log 2 hours on the API Integration project for database optimization"
78
+
79
+ **Get dashboard:**
80
+ > "What's my dashboard looking like? Show me active projects and upcoming tasks"
81
+
82
+ **Project summary:**
83
+ > "Give me a summary of the Mobile App project including tasks and time spent"
84
+
85
+ ## Environment Variables
86
+
87
+ | Variable | Required | Description |
88
+ |----------|----------|-------------|
89
+ | `PROJOFLOW_SUPABASE_URL` | ✅ | Your Supabase project URL |
90
+ | `PROJOFLOW_SUPABASE_ANON_KEY` | ✅ | Supabase anon/public key |
91
+ | `PROJOFLOW_MCP_EMAIL` | ✅ | MCP service account email |
92
+ | `PROJOFLOW_MCP_PASSWORD` | ✅ | MCP service account password |
93
+
94
+ ## Alternative: Global npm Install
95
+
96
+ ```bash
97
+ # From the mcp-server directory
98
+ npm install -g .
99
+
100
+ # Then in your config, use:
101
+ {
102
+ "mcpServers": {
103
+ "projoflow": {
104
+ "command": "projoflow-mcp",
105
+ "env": {
106
+ "PROJOFLOW_SUPABASE_URL": "...",
107
+ "PROJOFLOW_SUPABASE_ANON_KEY": "...",
108
+ "PROJOFLOW_MCP_EMAIL": "...",
109
+ "PROJOFLOW_MCP_PASSWORD": "..."
110
+ }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ## Cursor IDE Setup
117
+
118
+ Add to your Cursor settings (`.cursor/settings.json`):
119
+
120
+ ```json
121
+ {
122
+ "mcpServers": {
123
+ "projoflow": {
124
+ "command": "node",
125
+ "args": ["/path/to/projoflow/mcp-server/index.js"],
126
+ "env": {
127
+ "PROJOFLOW_SUPABASE_URL": "https://your-project.supabase.co",
128
+ "PROJOFLOW_SUPABASE_ANON_KEY": "your-anon-key",
129
+ "PROJOFLOW_MCP_EMAIL": "mcp@your-domain.com",
130
+ "PROJOFLOW_MCP_PASSWORD": "your-secure-password"
131
+ }
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## Security Notes
138
+
139
+ - The MCP user should have appropriate permissions in your ProjoFlow instance
140
+ - Store credentials securely; don't commit config files with real credentials
141
+ - Consider creating a dedicated MCP user with limited permissions
142
+ - The anon key is safe to use (it's a public key); authentication is handled via user login
143
+
144
+ ## Troubleshooting
145
+
146
+ **"Authentication failed"**
147
+ - Verify the email/password are correct
148
+ - Check the user exists in your ProjoFlow instance
149
+ - Ensure the user has confirmed their email
150
+
151
+ **"Missing environment variables"**
152
+ - All 4 environment variables are required
153
+ - Check for typos in variable names
154
+
155
+ **"Connection refused"**
156
+ - Verify your Supabase URL is correct
157
+ - Check your internet connection
158
+ - Ensure Supabase project is active
159
+
160
+ ## Support
161
+
162
+ For issues with the MCP server, check:
163
+ 1. ProjoFlow documentation
164
+ 2. Claude Code MCP documentation
165
+ 3. Supabase connection guides
166
+
167
+ ---
168
+
169
+ **ProjoFlow** - Project management built for the AI age 🚀
package/index.js ADDED
@@ -0,0 +1,641 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ProjoFlow MCP Server
5
+ *
6
+ * Connect your AI assistant (Claude Code, Cursor, etc.) to your ProjoFlow instance.
7
+ *
8
+ * Setup:
9
+ * 1. Create a user account in your ProjoFlow instance for the MCP server
10
+ * 2. Set environment variables (see below)
11
+ * 3. Add to your Claude Code / Cursor config
12
+ *
13
+ * Required Environment Variables:
14
+ * PROJOFLOW_SUPABASE_URL - Your Supabase project URL
15
+ * PROJOFLOW_SUPABASE_ANON_KEY - Your Supabase anon/public key
16
+ * PROJOFLOW_MCP_EMAIL - Email of the MCP service account user
17
+ * PROJOFLOW_MCP_PASSWORD - Password of the MCP service account user
18
+ */
19
+
20
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import {
23
+ CallToolRequestSchema,
24
+ ListToolsRequestSchema,
25
+ } from "@modelcontextprotocol/sdk/types.js";
26
+ import { createClient } from "@supabase/supabase-js";
27
+
28
+ // Configuration from environment variables
29
+ const SUPABASE_URL = process.env.PROJOFLOW_SUPABASE_URL;
30
+ const SUPABASE_ANON_KEY = process.env.PROJOFLOW_SUPABASE_ANON_KEY;
31
+ const MCP_EMAIL = process.env.PROJOFLOW_MCP_EMAIL;
32
+ const MCP_PASSWORD = process.env.PROJOFLOW_MCP_PASSWORD;
33
+
34
+ // Validate required environment variables
35
+ const missing = [];
36
+ if (!SUPABASE_URL) missing.push("PROJOFLOW_SUPABASE_URL");
37
+ if (!SUPABASE_ANON_KEY) missing.push("PROJOFLOW_SUPABASE_ANON_KEY");
38
+ if (!MCP_EMAIL) missing.push("PROJOFLOW_MCP_EMAIL");
39
+ if (!MCP_PASSWORD) missing.push("PROJOFLOW_MCP_PASSWORD");
40
+
41
+ if (missing.length > 0) {
42
+ console.error(`ERROR: Missing required environment variables: ${missing.join(", ")}`);
43
+ console.error("\nSetup instructions:");
44
+ console.error("1. Create a user in your ProjoFlow instance for MCP access");
45
+ console.error("2. Set these environment variables in your shell profile or Claude Code config:");
46
+ console.error(" PROJOFLOW_SUPABASE_URL=https://your-project.supabase.co");
47
+ console.error(" PROJOFLOW_SUPABASE_ANON_KEY=your-anon-key");
48
+ console.error(" PROJOFLOW_MCP_EMAIL=mcp@your-domain.com");
49
+ console.error(" PROJOFLOW_MCP_PASSWORD=your-password");
50
+ process.exit(1);
51
+ }
52
+
53
+ // Create Supabase client
54
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
55
+
56
+ // Authenticate on startup
57
+ async function authenticate() {
58
+ const { data, error } = await supabase.auth.signInWithPassword({
59
+ email: MCP_EMAIL,
60
+ password: MCP_PASSWORD
61
+ });
62
+
63
+ if (error) {
64
+ console.error(`Authentication failed: ${error.message}`);
65
+ console.error("Make sure the MCP user exists in your ProjoFlow instance.");
66
+ process.exit(1);
67
+ }
68
+
69
+ console.error(`✓ Authenticated as ${data.user.email}`);
70
+ return data;
71
+ }
72
+
73
+ // Tool definitions
74
+ const TOOLS = [
75
+ {
76
+ name: "list_projects",
77
+ description: "List all projects, optionally filtered by status",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ status: {
82
+ type: "string",
83
+ enum: ["draft", "active", "on_hold", "completed", "cancelled"],
84
+ description: "Filter by project status"
85
+ },
86
+ client_id: {
87
+ type: "string",
88
+ description: "Filter by client ID"
89
+ }
90
+ }
91
+ }
92
+ },
93
+ {
94
+ name: "get_project",
95
+ description: "Get details of a specific project including client info",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ project_id: { type: "string", description: "Project ID" }
100
+ },
101
+ required: ["project_id"]
102
+ }
103
+ },
104
+ {
105
+ name: "create_project",
106
+ description: "Create a new project",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ name: { type: "string", description: "Project name" },
111
+ description: { type: "string", description: "Project description" },
112
+ client_id: { type: "string", description: "Client ID" },
113
+ status: { type: "string", enum: ["draft", "active", "on_hold"], default: "draft" },
114
+ project_type: { type: "string", enum: ["automation", "internal_system", "mvp", "ai_agent", "consulting", "other"] },
115
+ budget_type: { type: "string", enum: ["fixed", "hourly", "retainer"] },
116
+ budget_amount: { type: "number" },
117
+ hourly_rate: { type: "number", default: 85 }
118
+ },
119
+ required: ["name"]
120
+ }
121
+ },
122
+ {
123
+ name: "update_project",
124
+ description: "Update a project's details or status",
125
+ inputSchema: {
126
+ type: "object",
127
+ properties: {
128
+ project_id: { type: "string", description: "Project ID" },
129
+ name: { type: "string" },
130
+ description: { type: "string" },
131
+ status: { type: "string", enum: ["draft", "active", "on_hold", "completed", "cancelled"] },
132
+ budget_amount: { type: "number" },
133
+ hourly_rate: { type: "number" }
134
+ },
135
+ required: ["project_id"]
136
+ }
137
+ },
138
+ {
139
+ name: "list_tasks",
140
+ description: "List tasks for a project, optionally filtered by status",
141
+ inputSchema: {
142
+ type: "object",
143
+ properties: {
144
+ project_id: { type: "string", description: "Project ID (required)" },
145
+ status: { type: "string", enum: ["todo", "in_progress", "review", "done"] }
146
+ },
147
+ required: ["project_id"]
148
+ }
149
+ },
150
+ {
151
+ name: "create_task",
152
+ description: "Create a new task in a project",
153
+ inputSchema: {
154
+ type: "object",
155
+ properties: {
156
+ project_id: { type: "string", description: "Project ID" },
157
+ title: { type: "string", description: "Task title" },
158
+ description: { type: "string", description: "Task description" },
159
+ status: { type: "string", enum: ["todo", "in_progress", "review", "done"], default: "todo" },
160
+ priority: { type: "string", enum: ["low", "medium", "high", "urgent"], default: "medium" },
161
+ due_date: { type: "string", description: "Due date (YYYY-MM-DD)" },
162
+ estimated_hours: { type: "number" }
163
+ },
164
+ required: ["project_id", "title"]
165
+ }
166
+ },
167
+ {
168
+ name: "update_task",
169
+ description: "Update a task's details or status",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ task_id: { type: "string", description: "Task ID" },
174
+ title: { type: "string" },
175
+ description: { type: "string" },
176
+ status: { type: "string", enum: ["todo", "in_progress", "review", "done"] },
177
+ priority: { type: "string", enum: ["low", "medium", "high", "urgent"] },
178
+ due_date: { type: "string" }
179
+ },
180
+ required: ["task_id"]
181
+ }
182
+ },
183
+ {
184
+ name: "log_time",
185
+ description: "Log time worked on a project",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: {
189
+ project_id: { type: "string", description: "Project ID" },
190
+ task_id: { type: "string", description: "Task ID (optional)" },
191
+ duration_minutes: { type: "number", description: "Duration in minutes" },
192
+ description: { type: "string", description: "What was done" },
193
+ date: { type: "string", description: "Date (YYYY-MM-DD), defaults to today" },
194
+ billable: { type: "boolean", default: true }
195
+ },
196
+ required: ["project_id", "duration_minutes"]
197
+ }
198
+ },
199
+ {
200
+ name: "get_time_entries",
201
+ description: "Get time entries for a project or date range",
202
+ inputSchema: {
203
+ type: "object",
204
+ properties: {
205
+ project_id: { type: "string", description: "Filter by project" },
206
+ start_date: { type: "string", description: "Start date (YYYY-MM-DD)" },
207
+ end_date: { type: "string", description: "End date (YYYY-MM-DD)" },
208
+ limit: { type: "number", default: 50 }
209
+ }
210
+ }
211
+ },
212
+ {
213
+ name: "list_clients",
214
+ description: "List all clients",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ search: { type: "string", description: "Search by name" }
219
+ }
220
+ }
221
+ },
222
+ {
223
+ name: "create_client",
224
+ description: "Create a new client",
225
+ inputSchema: {
226
+ type: "object",
227
+ properties: {
228
+ name: { type: "string", description: "Client/Company name" },
229
+ contact_name: { type: "string", description: "Contact person" },
230
+ email: { type: "string" },
231
+ phone: { type: "string" },
232
+ company: { type: "string" },
233
+ notes: { type: "string" }
234
+ },
235
+ required: ["name"]
236
+ }
237
+ },
238
+ {
239
+ name: "list_leads",
240
+ description: "List leads/inquiries, optionally filtered by status",
241
+ inputSchema: {
242
+ type: "object",
243
+ properties: {
244
+ status: { type: "string", enum: ["new", "contacted", "qualified", "converted", "lost"] },
245
+ limit: { type: "number", default: 20 }
246
+ }
247
+ }
248
+ },
249
+ {
250
+ name: "update_lead",
251
+ description: "Update a lead's status or notes",
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ lead_id: { type: "string", description: "Lead ID" },
256
+ status: { type: "string", enum: ["new", "contacted", "qualified", "converted", "lost"] },
257
+ notes: { type: "string" }
258
+ },
259
+ required: ["lead_id"]
260
+ }
261
+ },
262
+ {
263
+ name: "add_note",
264
+ description: "Add a note to a project",
265
+ inputSchema: {
266
+ type: "object",
267
+ properties: {
268
+ project_id: { type: "string", description: "Project ID" },
269
+ title: { type: "string", description: "Note title" },
270
+ content: { type: "string", description: "Note content" },
271
+ note_type: { type: "string", enum: ["general", "meeting", "technical", "decision"], default: "general" }
272
+ },
273
+ required: ["project_id", "title"]
274
+ }
275
+ },
276
+ {
277
+ name: "get_project_summary",
278
+ description: "Get a summary of a project including tasks, time, and notes",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ project_id: { type: "string", description: "Project ID" }
283
+ },
284
+ required: ["project_id"]
285
+ }
286
+ },
287
+ {
288
+ name: "get_dashboard",
289
+ description: "Get dashboard overview: active projects, recent time entries, upcoming tasks",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {}
293
+ }
294
+ },
295
+ {
296
+ name: "list_task_comments",
297
+ description: "Get comments for a specific task",
298
+ inputSchema: {
299
+ type: "object",
300
+ properties: {
301
+ task_id: { type: "string", description: "Task ID" }
302
+ },
303
+ required: ["task_id"]
304
+ }
305
+ },
306
+ {
307
+ name: "add_task_comment",
308
+ description: "Add a comment to a task",
309
+ inputSchema: {
310
+ type: "object",
311
+ properties: {
312
+ task_id: { type: "string", description: "Task ID" },
313
+ content: { type: "string", description: "Comment content" }
314
+ },
315
+ required: ["task_id", "content"]
316
+ }
317
+ }
318
+ ];
319
+
320
+ // Tool handlers
321
+ async function handleTool(name, args) {
322
+ switch (name) {
323
+ case "list_projects": {
324
+ let query = supabase.from("projects").select("*, clients(name)").order("created_at", { ascending: false });
325
+ if (args.status) query = query.eq("status", args.status);
326
+ if (args.client_id) query = query.eq("client_id", args.client_id);
327
+ const { data, error } = await query;
328
+ if (error) throw new Error(error.message);
329
+ return data;
330
+ }
331
+
332
+ case "get_project": {
333
+ const { data, error } = await supabase
334
+ .from("projects")
335
+ .select("*, clients(name, email)")
336
+ .eq("id", args.project_id)
337
+ .single();
338
+ if (error) throw new Error(error.message);
339
+ return data;
340
+ }
341
+
342
+ case "create_project": {
343
+ const { data, error } = await supabase
344
+ .from("projects")
345
+ .insert({
346
+ name: args.name,
347
+ description: args.description,
348
+ client_id: args.client_id,
349
+ status: args.status || "draft",
350
+ project_type: args.project_type || "other",
351
+ budget_type: args.budget_type || "hourly",
352
+ budget_amount: args.budget_amount,
353
+ hourly_rate: args.hourly_rate || 85
354
+ })
355
+ .select()
356
+ .single();
357
+ if (error) throw new Error(error.message);
358
+ return data;
359
+ }
360
+
361
+ case "update_project": {
362
+ const { project_id, ...updates } = args;
363
+ const { data, error } = await supabase
364
+ .from("projects")
365
+ .update(updates)
366
+ .eq("id", project_id)
367
+ .select()
368
+ .single();
369
+ if (error) throw new Error(error.message);
370
+ return data;
371
+ }
372
+
373
+ case "list_tasks": {
374
+ let query = supabase.from("tasks")
375
+ .select("*")
376
+ .eq("project_id", args.project_id)
377
+ .order("position").order("created_at");
378
+ if (args.status) query = query.eq("status", args.status);
379
+ const { data, error } = await query;
380
+ if (error) throw new Error(error.message);
381
+ return data;
382
+ }
383
+
384
+ case "create_task": {
385
+ const { data, error } = await supabase
386
+ .from("tasks")
387
+ .insert({
388
+ project_id: args.project_id,
389
+ title: args.title,
390
+ description: args.description,
391
+ status: args.status || "todo",
392
+ priority: args.priority || "medium",
393
+ due_date: args.due_date,
394
+ estimated_hours: args.estimated_hours
395
+ })
396
+ .select()
397
+ .single();
398
+ if (error) throw new Error(error.message);
399
+ return data;
400
+ }
401
+
402
+ case "update_task": {
403
+ const { task_id, ...updates } = args;
404
+ const { data, error } = await supabase
405
+ .from("tasks")
406
+ .update(updates)
407
+ .eq("id", task_id)
408
+ .select()
409
+ .single();
410
+ if (error) throw new Error(error.message);
411
+ return data;
412
+ }
413
+
414
+ case "log_time": {
415
+ const { data, error } = await supabase
416
+ .from("time_entries")
417
+ .insert({
418
+ project_id: args.project_id,
419
+ task_id: args.task_id,
420
+ duration_minutes: args.duration_minutes,
421
+ description: args.description,
422
+ date: args.date || new Date().toISOString().split("T")[0],
423
+ billable: args.billable !== false
424
+ })
425
+ .select("*, projects(name)")
426
+ .single();
427
+ if (error) throw new Error(error.message);
428
+ return data;
429
+ }
430
+
431
+ case "get_time_entries": {
432
+ let query = supabase.from("time_entries")
433
+ .select("*, projects(name), tasks(title)")
434
+ .order("date", { ascending: false })
435
+ .limit(args.limit || 50);
436
+ if (args.project_id) query = query.eq("project_id", args.project_id);
437
+ if (args.start_date) query = query.gte("date", args.start_date);
438
+ if (args.end_date) query = query.lte("date", args.end_date);
439
+ const { data, error } = await query;
440
+ if (error) throw new Error(error.message);
441
+ return data;
442
+ }
443
+
444
+ case "list_clients": {
445
+ let query = supabase.from("clients").select("*").order("name");
446
+ if (args.search) query = query.ilike("name", `%${args.search}%`);
447
+ const { data, error } = await query;
448
+ if (error) throw new Error(error.message);
449
+ return data;
450
+ }
451
+
452
+ case "create_client": {
453
+ const { data, error } = await supabase
454
+ .from("clients")
455
+ .insert({
456
+ name: args.name,
457
+ contact_name: args.contact_name,
458
+ email: args.email,
459
+ phone: args.phone,
460
+ company: args.company,
461
+ notes: args.notes
462
+ })
463
+ .select()
464
+ .single();
465
+ if (error) throw new Error(error.message);
466
+ return data;
467
+ }
468
+
469
+ case "list_leads": {
470
+ let query = supabase.from("leads")
471
+ .select("*")
472
+ .order("created_at", { ascending: false })
473
+ .limit(args.limit || 20);
474
+ if (args.status) query = query.eq("status", args.status);
475
+ const { data, error } = await query;
476
+ if (error) throw new Error(error.message);
477
+ return data;
478
+ }
479
+
480
+ case "update_lead": {
481
+ const { lead_id, ...updates } = args;
482
+ const { data, error } = await supabase
483
+ .from("leads")
484
+ .update(updates)
485
+ .eq("id", lead_id)
486
+ .select()
487
+ .single();
488
+ if (error) throw new Error(error.message);
489
+ return data;
490
+ }
491
+
492
+ case "add_note": {
493
+ const { data, error } = await supabase
494
+ .from("notes")
495
+ .insert({
496
+ project_id: args.project_id,
497
+ title: args.title,
498
+ content: args.content,
499
+ note_type: args.note_type || "general"
500
+ })
501
+ .select()
502
+ .single();
503
+ if (error) throw new Error(error.message);
504
+ return data;
505
+ }
506
+
507
+ case "get_project_summary": {
508
+ const [project, tasks, timeEntries, notes] = await Promise.all([
509
+ supabase.from("projects").select("*, clients(name)").eq("id", args.project_id).single(),
510
+ supabase.from("tasks").select("*").eq("project_id", args.project_id),
511
+ supabase.from("time_entries").select("*").eq("project_id", args.project_id),
512
+ supabase.from("notes").select("*").eq("project_id", args.project_id).order("created_at", { ascending: false }).limit(5)
513
+ ]);
514
+
515
+ if (project.error) throw new Error(project.error.message);
516
+
517
+ const tasksByStatus = {
518
+ todo: tasks.data?.filter(t => t.status === "todo").length || 0,
519
+ in_progress: tasks.data?.filter(t => t.status === "in_progress").length || 0,
520
+ review: tasks.data?.filter(t => t.status === "review").length || 0,
521
+ done: tasks.data?.filter(t => t.status === "done").length || 0
522
+ };
523
+
524
+ const totalMinutes = timeEntries.data?.reduce((sum, e) => sum + e.duration_minutes, 0) || 0;
525
+ const billableMinutes = timeEntries.data?.filter(e => e.billable).reduce((sum, e) => sum + e.duration_minutes, 0) || 0;
526
+
527
+ return {
528
+ project: project.data,
529
+ tasks: {
530
+ total: tasks.data?.length || 0,
531
+ by_status: tasksByStatus,
532
+ items: tasks.data
533
+ },
534
+ time: {
535
+ total_hours: (totalMinutes / 60).toFixed(1),
536
+ billable_hours: (billableMinutes / 60).toFixed(1),
537
+ billable_amount: ((billableMinutes / 60) * (project.data.hourly_rate || 85)).toFixed(2)
538
+ },
539
+ recent_notes: notes.data
540
+ };
541
+ }
542
+
543
+ case "get_dashboard": {
544
+ const [projects, recentTime, leads] = await Promise.all([
545
+ supabase.from("projects").select("*, clients(name)").in("status", ["active", "on_hold"]).order("updated_at", { ascending: false }),
546
+ supabase.from("time_entries").select("*, projects(name)").order("date", { ascending: false }).limit(10),
547
+ supabase.from("leads").select("*").eq("status", "new").order("created_at", { ascending: false }).limit(5)
548
+ ]);
549
+
550
+ // Get tasks due soon
551
+ const today = new Date().toISOString().split("T")[0];
552
+ const nextWeek = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
553
+ const upcomingTasks = await supabase
554
+ .from("tasks")
555
+ .select("*, projects(name)")
556
+ .neq("status", "done")
557
+ .gte("due_date", today)
558
+ .lte("due_date", nextWeek)
559
+ .order("due_date");
560
+
561
+ return {
562
+ active_projects: projects.data,
563
+ recent_time_entries: recentTime.data,
564
+ new_leads: leads.data,
565
+ upcoming_tasks: upcomingTasks.data
566
+ };
567
+ }
568
+
569
+ case "list_task_comments": {
570
+ const { data, error } = await supabase
571
+ .from("task_comments")
572
+ .select("*")
573
+ .eq("task_id", args.task_id)
574
+ .order("created_at", { ascending: true });
575
+ if (error) throw new Error(error.message);
576
+ return data;
577
+ }
578
+
579
+ case "add_task_comment": {
580
+ // Get current user info
581
+ const { data: { user } } = await supabase.auth.getUser();
582
+ if (!user) throw new Error("Not authenticated");
583
+
584
+ const { data, error } = await supabase
585
+ .from("task_comments")
586
+ .insert({
587
+ task_id: args.task_id,
588
+ user_id: user.id,
589
+ content: args.content,
590
+ author_type: "admin",
591
+ author_name: user.email || "MCP Assistant"
592
+ })
593
+ .select()
594
+ .single();
595
+ if (error) throw new Error(error.message);
596
+ return data;
597
+ }
598
+
599
+ default:
600
+ throw new Error(`Unknown tool: ${name}`);
601
+ }
602
+ }
603
+
604
+ // Create MCP server
605
+ const server = new Server(
606
+ { name: "projoflow-mcp", version: "1.0.0" },
607
+ { capabilities: { tools: {} } }
608
+ );
609
+
610
+ // Register handlers
611
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
612
+ tools: TOOLS
613
+ }));
614
+
615
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
616
+ const { name, arguments: args } = request.params;
617
+ try {
618
+ const result = await handleTool(name, args || {});
619
+ return {
620
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
621
+ };
622
+ } catch (error) {
623
+ return {
624
+ content: [{ type: "text", text: `Error: ${error.message}` }],
625
+ isError: true
626
+ };
627
+ }
628
+ });
629
+
630
+ // Start server with authentication
631
+ async function main() {
632
+ await authenticate();
633
+ const transport = new StdioServerTransport();
634
+ await server.connect(transport);
635
+ console.error("✓ ProjoFlow MCP server running");
636
+ }
637
+
638
+ main().catch(err => {
639
+ console.error(`Fatal error: ${err.message}`);
640
+ process.exit(1);
641
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "projoflow-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for ProjoFlow - connect your AI assistant (Claude, Cursor) to your project management",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "projoflow-mcp": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "project-management",
17
+ "projoflow",
18
+ "claude",
19
+ "cursor",
20
+ "ai-assistant",
21
+ "supabase"
22
+ ],
23
+ "author": "ProjoFlow <hello@projoflow.com>",
24
+ "license": "MIT",
25
+ "homepage": "https://projoflow.com",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/mahmoudsheikh94/projoflow-selfhosted.git",
29
+ "directory": "mcp-server"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/mahmoudsheikh94/projoflow-selfhosted/issues"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.0.0",
39
+ "@supabase/supabase-js": "^2.0.0"
40
+ }
41
+ }