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.
- package/README.md +169 -0
- package/index.js +641 -0
- 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
|
+
}
|