@dalmasonto/taskflow-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/README.md +184 -0
- package/dist/db.d.ts +5 -0
- package/dist/db.js +109 -0
- package/dist/helpers.d.ts +21 -0
- package/dist/helpers.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +54 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.js +282 -0
- package/dist/tools/activity.d.ts +28 -0
- package/dist/tools/activity.js +56 -0
- package/dist/tools/agent.d.ts +14 -0
- package/dist/tools/agent.js +85 -0
- package/dist/tools/analytics.d.ts +21 -0
- package/dist/tools/analytics.js +108 -0
- package/dist/tools/notifications.d.ts +31 -0
- package/dist/tools/notifications.js +59 -0
- package/dist/tools/projects.d.ts +55 -0
- package/dist/tools/projects.js +112 -0
- package/dist/tools/settings.d.ts +19 -0
- package/dist/tools/settings.js +52 -0
- package/dist/tools/tasks.d.ts +103 -0
- package/dist/tools/tasks.js +351 -0
- package/dist/tools/timer.d.ts +37 -0
- package/dist/tools/timer.js +131 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +24 -0
- package/package.json +56 -0
package/dist/sse.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { getDb } from './db.js';
|
|
3
|
+
import { logActivity } from './helpers.js';
|
|
4
|
+
const clients = new Set();
|
|
5
|
+
function resolvePort() {
|
|
6
|
+
// CLI arg takes priority: --port 4000
|
|
7
|
+
const portArgIdx = process.argv.indexOf('--port');
|
|
8
|
+
if (portArgIdx !== -1 && process.argv[portArgIdx + 1]) {
|
|
9
|
+
return parseInt(process.argv[portArgIdx + 1], 10);
|
|
10
|
+
}
|
|
11
|
+
// Then env var
|
|
12
|
+
if (process.env.TASKFLOW_SSE_PORT) {
|
|
13
|
+
return parseInt(process.env.TASKFLOW_SSE_PORT, 10);
|
|
14
|
+
}
|
|
15
|
+
return 3456;
|
|
16
|
+
}
|
|
17
|
+
const PORT = resolvePort();
|
|
18
|
+
function jsonResponse(res, status, data) {
|
|
19
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
20
|
+
res.end(JSON.stringify(data));
|
|
21
|
+
}
|
|
22
|
+
function readBody(req) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let body = '';
|
|
25
|
+
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
26
|
+
req.on('end', () => resolve(body));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function startSSEServer() {
|
|
30
|
+
const server = createServer(async (req, res) => {
|
|
31
|
+
// CORS headers for all requests
|
|
32
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
33
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
|
|
34
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
35
|
+
if (req.method === 'OPTIONS') {
|
|
36
|
+
res.writeHead(204);
|
|
37
|
+
res.end();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (req.url === '/events' && req.method === 'GET') {
|
|
41
|
+
res.writeHead(200, {
|
|
42
|
+
'Content-Type': 'text/event-stream',
|
|
43
|
+
'Cache-Control': 'no-cache',
|
|
44
|
+
'Connection': 'keep-alive',
|
|
45
|
+
});
|
|
46
|
+
// Send initial connection event
|
|
47
|
+
res.write('event: connected\ndata: {}\n\n');
|
|
48
|
+
clients.add(res);
|
|
49
|
+
req.on('close', () => {
|
|
50
|
+
clients.delete(res);
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (req.url === '/sync' && req.method === 'GET') {
|
|
55
|
+
const db = getDb();
|
|
56
|
+
const tasks = db.prepare('SELECT * FROM tasks').all();
|
|
57
|
+
const projects = db.prepare('SELECT * FROM projects').all();
|
|
58
|
+
const sessions = db.prepare('SELECT * FROM sessions').all();
|
|
59
|
+
const settings = db.prepare('SELECT * FROM settings').all();
|
|
60
|
+
const activityLogs = db.prepare('SELECT * FROM activity_logs ORDER BY created_at DESC LIMIT 200').all();
|
|
61
|
+
jsonResponse(res, 200, { tasks, projects, sessions, settings, activityLogs });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// ─── Mutation endpoints ───────────────────────────────────────────
|
|
65
|
+
if (req.url === '/api/clear-data' && req.method === 'POST') {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
db.exec('DELETE FROM sessions');
|
|
68
|
+
db.exec('DELETE FROM tasks');
|
|
69
|
+
db.exec('DELETE FROM projects');
|
|
70
|
+
db.exec('DELETE FROM notifications');
|
|
71
|
+
db.exec('DELETE FROM activity_logs');
|
|
72
|
+
logActivity('data_cleared', 'All data cleared via UI', { entityType: 'system' });
|
|
73
|
+
broadcast('data_cleared', { entity: 'system', action: 'data_cleared', payload: {} });
|
|
74
|
+
jsonResponse(res, 200, { cleared: true });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// PATCH /api/tasks/:id — partial update
|
|
78
|
+
const taskPatchMatch = req.url?.match(/^\/api\/tasks\/(\d+)$/);
|
|
79
|
+
if (taskPatchMatch && req.method === 'PATCH') {
|
|
80
|
+
const db = getDb();
|
|
81
|
+
const id = Number(taskPatchMatch[1]);
|
|
82
|
+
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
83
|
+
if (!task) {
|
|
84
|
+
jsonResponse(res, 404, { error: 'Task not found' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const body = JSON.parse(await readBody(req));
|
|
88
|
+
const fieldMap = {
|
|
89
|
+
title: 'title', description: 'description', status: 'status',
|
|
90
|
+
priority: 'priority', projectId: 'project_id', dueDate: 'due_date',
|
|
91
|
+
estimatedTime: 'estimated_time', dependencies: 'dependencies',
|
|
92
|
+
tags: 'tags', links: 'links',
|
|
93
|
+
};
|
|
94
|
+
const sets = [];
|
|
95
|
+
const vals = [];
|
|
96
|
+
for (const [camel, col] of Object.entries(fieldMap)) {
|
|
97
|
+
if (body[camel] !== undefined) {
|
|
98
|
+
const val = body[camel];
|
|
99
|
+
if (col === 'dependencies' || col === 'tags' || col === 'links') {
|
|
100
|
+
sets.push(`${col} = ?`);
|
|
101
|
+
vals.push(JSON.stringify(val));
|
|
102
|
+
}
|
|
103
|
+
else if (col === 'due_date' && val) {
|
|
104
|
+
sets.push(`${col} = ?`);
|
|
105
|
+
vals.push(new Date(val).toISOString());
|
|
106
|
+
}
|
|
107
|
+
else if (col === 'project_id' && (val === null || val === undefined)) {
|
|
108
|
+
sets.push(`${col} = ?`);
|
|
109
|
+
vals.push(null);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sets.push(`${col} = ?`);
|
|
113
|
+
vals.push(val);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (sets.length > 0) {
|
|
118
|
+
sets.push('updated_at = ?');
|
|
119
|
+
vals.push(new Date().toISOString());
|
|
120
|
+
db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id);
|
|
121
|
+
}
|
|
122
|
+
const updated = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
123
|
+
broadcast('task_updated', { entity: 'task', action: 'task_updated', payload: updated });
|
|
124
|
+
jsonResponse(res, 200, updated);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// DELETE /api/tasks/:id
|
|
128
|
+
const taskDeleteMatch = req.url?.match(/^\/api\/tasks\/(\d+)$/);
|
|
129
|
+
if (taskDeleteMatch && req.method === 'DELETE') {
|
|
130
|
+
const db = getDb();
|
|
131
|
+
const id = Number(taskDeleteMatch[1]);
|
|
132
|
+
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
133
|
+
if (!task) {
|
|
134
|
+
jsonResponse(res, 404, { error: 'Task not found' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
138
|
+
logActivity('task_deleted', task.title, { entityType: 'task', entityId: id });
|
|
139
|
+
broadcast('task_deleted', { entity: 'task', action: 'task_deleted', payload: { id } });
|
|
140
|
+
jsonResponse(res, 200, { deleted: true, id });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// POST /api/tasks — create a task
|
|
144
|
+
if (req.url === '/api/tasks' && req.method === 'POST') {
|
|
145
|
+
const db = getDb();
|
|
146
|
+
const body = JSON.parse(await readBody(req));
|
|
147
|
+
const ts = new Date().toISOString();
|
|
148
|
+
const result = db.prepare(`INSERT INTO tasks (title, description, status, priority, project_id, dependencies, links, tags, due_date, estimated_time, created_at, updated_at)
|
|
149
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(body.title, body.description ?? null, body.status ?? 'not_started', body.priority ?? 'medium', body.projectId ?? null, JSON.stringify(body.dependencies ?? []), JSON.stringify(body.links ?? []), JSON.stringify(body.tags ?? []), body.dueDate ? new Date(body.dueDate).toISOString() : null, body.estimatedTime ?? null, ts, ts);
|
|
150
|
+
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid);
|
|
151
|
+
logActivity('task_created', body.title, { entityType: 'task', entityId: result.lastInsertRowid });
|
|
152
|
+
broadcast('task_created', { entity: 'task', action: 'task_created', payload: task });
|
|
153
|
+
jsonResponse(res, 201, task);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// POST /api/sessions — create a timer session
|
|
157
|
+
if (req.url === '/api/sessions' && req.method === 'POST') {
|
|
158
|
+
const db = getDb();
|
|
159
|
+
const body = JSON.parse(await readBody(req));
|
|
160
|
+
const ts = new Date().toISOString();
|
|
161
|
+
const result = db.prepare('INSERT INTO sessions (task_id, start, end) VALUES (?, ?, ?)').run(body.taskId, body.start ? new Date(body.start).toISOString() : ts, body.end ? new Date(body.end).toISOString() : null);
|
|
162
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(result.lastInsertRowid);
|
|
163
|
+
broadcast('timer_started', { entity: 'timer', action: 'timer_started', payload: { task_id: body.taskId, session } });
|
|
164
|
+
jsonResponse(res, 201, session);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// PATCH /api/sessions/:id — update session (close it)
|
|
168
|
+
const sessionPatchMatch = req.url?.match(/^\/api\/sessions\/(\d+)$/);
|
|
169
|
+
if (sessionPatchMatch && req.method === 'PATCH') {
|
|
170
|
+
const db = getDb();
|
|
171
|
+
const id = Number(sessionPatchMatch[1]);
|
|
172
|
+
const body = JSON.parse(await readBody(req));
|
|
173
|
+
if (body.end) {
|
|
174
|
+
db.prepare('UPDATE sessions SET end = ? WHERE id = ?').run(new Date(body.end).toISOString(), id);
|
|
175
|
+
}
|
|
176
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
177
|
+
jsonResponse(res, 200, session);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// PATCH /api/projects/:id — partial update
|
|
181
|
+
const projectPatchMatch = req.url?.match(/^\/api\/projects\/(\d+)$/);
|
|
182
|
+
if (projectPatchMatch && req.method === 'PATCH') {
|
|
183
|
+
const db = getDb();
|
|
184
|
+
const id = Number(projectPatchMatch[1]);
|
|
185
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
|
186
|
+
if (!project) {
|
|
187
|
+
jsonResponse(res, 404, { error: 'Project not found' });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const body = JSON.parse(await readBody(req));
|
|
191
|
+
const fieldMap = {
|
|
192
|
+
name: 'name', color: 'color', type: 'type', description: 'description',
|
|
193
|
+
};
|
|
194
|
+
const sets = [];
|
|
195
|
+
const vals = [];
|
|
196
|
+
for (const [camel, col] of Object.entries(fieldMap)) {
|
|
197
|
+
if (body[camel] !== undefined) {
|
|
198
|
+
sets.push(`${col} = ?`);
|
|
199
|
+
vals.push(body[camel]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (sets.length > 0) {
|
|
203
|
+
sets.push('updated_at = ?');
|
|
204
|
+
vals.push(new Date().toISOString());
|
|
205
|
+
db.prepare(`UPDATE projects SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id);
|
|
206
|
+
}
|
|
207
|
+
const updated = db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
|
208
|
+
broadcast('project_updated', { entity: 'project', action: 'project_updated', payload: updated });
|
|
209
|
+
jsonResponse(res, 200, updated);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// DELETE /api/projects/:id
|
|
213
|
+
const projectDeleteMatch = req.url?.match(/^\/api\/projects\/(\d+)$/);
|
|
214
|
+
if (projectDeleteMatch && req.method === 'DELETE') {
|
|
215
|
+
const db = getDb();
|
|
216
|
+
const id = Number(projectDeleteMatch[1]);
|
|
217
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
|
218
|
+
if (!project) {
|
|
219
|
+
jsonResponse(res, 404, { error: 'Project not found' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
db.prepare('DELETE FROM projects WHERE id = ?').run(id);
|
|
223
|
+
logActivity('project_deleted', project.name, { entityType: 'project', entityId: id });
|
|
224
|
+
broadcast('project_deleted', { entity: 'project', action: 'project_deleted', payload: { id } });
|
|
225
|
+
jsonResponse(res, 200, { deleted: true, id });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// POST /api/broadcast — relay SSE events from other processes (e.g. MCP)
|
|
229
|
+
if (req.url === '/api/broadcast' && req.method === 'POST') {
|
|
230
|
+
const body = JSON.parse(await readBody(req));
|
|
231
|
+
if (body.event && body.data) {
|
|
232
|
+
broadcastLocal(body.event, body.data);
|
|
233
|
+
}
|
|
234
|
+
jsonResponse(res, 200, { relayed: true });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
res.writeHead(404);
|
|
238
|
+
res.end('Not Found');
|
|
239
|
+
});
|
|
240
|
+
server.on('error', (err) => {
|
|
241
|
+
if (err.code === 'EADDRINUSE') {
|
|
242
|
+
// Another MCP instance already owns this port — skip SSE, MCP tools still work
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Unexpected error — still don't crash the MCP process
|
|
246
|
+
});
|
|
247
|
+
server.listen(PORT, '0.0.0.0', () => {
|
|
248
|
+
markSSEActive();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/** Broadcast directly to connected SSE clients in this process */
|
|
252
|
+
function broadcastLocal(event, data) {
|
|
253
|
+
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
254
|
+
for (const client of clients) {
|
|
255
|
+
client.write(message);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Track whether this process owns the SSE server */
|
|
259
|
+
let sseServerActive = false;
|
|
260
|
+
export function markSSEActive() {
|
|
261
|
+
sseServerActive = true;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Broadcast an SSE event. If this process owns the SSE server, send directly.
|
|
265
|
+
* Otherwise, relay via HTTP to the process that does (sidecar on port 3456).
|
|
266
|
+
*/
|
|
267
|
+
export function broadcast(event, data) {
|
|
268
|
+
if (sseServerActive && clients.size > 0) {
|
|
269
|
+
broadcastLocal(event, data);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// Relay to the SSE server owner via HTTP
|
|
273
|
+
const body = JSON.stringify({ event, data });
|
|
274
|
+
fetch(`http://localhost:${PORT}/api/broadcast`, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: { 'Content-Type': 'application/json' },
|
|
277
|
+
body,
|
|
278
|
+
}).catch(() => {
|
|
279
|
+
// SSE server not running — silently skip
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export declare function getActivityLog(params: {
|
|
3
|
+
limit?: number;
|
|
4
|
+
action?: string;
|
|
5
|
+
entity_type?: string;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
content: {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}[];
|
|
11
|
+
}>;
|
|
12
|
+
export declare function clearActivityLog(): Promise<{
|
|
13
|
+
content: {
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
}[];
|
|
17
|
+
}>;
|
|
18
|
+
export declare function logDebug(params: {
|
|
19
|
+
message: string;
|
|
20
|
+
task_id?: number;
|
|
21
|
+
detail?: string;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
content: {
|
|
24
|
+
type: "text";
|
|
25
|
+
text: string;
|
|
26
|
+
}[];
|
|
27
|
+
}>;
|
|
28
|
+
export declare function registerActivityTools(server: McpServer): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getDb } from '../db.js';
|
|
3
|
+
import { successResponse, broadcastChange } from '../helpers.js';
|
|
4
|
+
// ─── exported handler functions ───────────────────────────────────────
|
|
5
|
+
export async function getActivityLog(params) {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
const limit = params.limit ?? 50;
|
|
8
|
+
const conditions = [];
|
|
9
|
+
const values = [];
|
|
10
|
+
if (params.action) {
|
|
11
|
+
conditions.push('action = ?');
|
|
12
|
+
values.push(params.action);
|
|
13
|
+
}
|
|
14
|
+
if (params.entity_type) {
|
|
15
|
+
conditions.push('entity_type = ?');
|
|
16
|
+
values.push(params.entity_type);
|
|
17
|
+
}
|
|
18
|
+
let sql = 'SELECT * FROM activity_logs';
|
|
19
|
+
if (conditions.length > 0) {
|
|
20
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
21
|
+
}
|
|
22
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
23
|
+
values.push(limit);
|
|
24
|
+
const rows = db.prepare(sql).all(...values);
|
|
25
|
+
return successResponse(rows);
|
|
26
|
+
}
|
|
27
|
+
export async function clearActivityLog() {
|
|
28
|
+
const db = getDb();
|
|
29
|
+
const countRow = db.prepare('SELECT COUNT(*) AS count FROM activity_logs').get();
|
|
30
|
+
db.prepare('DELETE FROM activity_logs').run();
|
|
31
|
+
broadcastChange('activity', 'activity_cleared', {});
|
|
32
|
+
return successResponse({ deleted: countRow.count, message: 'Activity log cleared' });
|
|
33
|
+
}
|
|
34
|
+
export async function logDebug(params) {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const result = db.prepare(`INSERT INTO activity_logs (action, title, detail, entity_type, entity_id, created_at)
|
|
38
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run('debug_log', params.message, params.detail ?? null, params.task_id ? 'task' : null, params.task_id ?? null, now);
|
|
39
|
+
const entry = db.prepare('SELECT * FROM activity_logs WHERE id = ?').get(result.lastInsertRowid);
|
|
40
|
+
broadcastChange('activity', 'activity_logged', entry);
|
|
41
|
+
return successResponse({ id: result.lastInsertRowid, message: params.message });
|
|
42
|
+
}
|
|
43
|
+
// ─── MCP registration ─────────────────────────────────────────────────
|
|
44
|
+
export function registerActivityTools(server) {
|
|
45
|
+
server.tool('get_activity_log', 'Retrieve recent activity log entries. Shows what has changed — task completions, timer events, status transitions. Filter by action or entity type.', {
|
|
46
|
+
limit: z.number().optional(),
|
|
47
|
+
action: z.string().optional(),
|
|
48
|
+
entity_type: z.string().optional(),
|
|
49
|
+
}, async (params) => getActivityLog(params));
|
|
50
|
+
server.tool('clear_activity_log', 'Delete all activity log entries. Use with caution — this is irreversible.', {}, async () => clearActivityLog());
|
|
51
|
+
server.tool('log_debug', 'Log a debug entry to the activity log. Use this while debugging to record what you are investigating, what you tried, what you found, and your reasoning. Optionally link to a task. These entries appear in the Activity Pulse in the UI.', {
|
|
52
|
+
message: z.string().describe('Short summary of what you are doing or found'),
|
|
53
|
+
detail: z.string().optional().describe('Longer explanation — stack traces, error messages, hypotheses, what you tried'),
|
|
54
|
+
task_id: z.number().optional().describe('Link this debug log to a specific task'),
|
|
55
|
+
}, async (params) => logDebug(params));
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export declare function getAgentInstructions(): Promise<{
|
|
3
|
+
content: {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
}>;
|
|
8
|
+
export declare function clearData(): Promise<{
|
|
9
|
+
content: {
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
}>;
|
|
14
|
+
export declare function registerAgentTools(server: McpServer): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getDb } from '../db.js';
|
|
2
|
+
import { logActivity, successResponse, broadcastChange } from '../helpers.js';
|
|
3
|
+
// ─── exported handler functions ───────────────────────────────────────
|
|
4
|
+
export async function getAgentInstructions() {
|
|
5
|
+
const db = getDb();
|
|
6
|
+
// Gather live context to include in the instructions
|
|
7
|
+
const projectCount = db.prepare('SELECT COUNT(*) AS c FROM projects').get().c;
|
|
8
|
+
const taskCount = db.prepare('SELECT COUNT(*) AS c FROM tasks').get().c;
|
|
9
|
+
const inProgressCount = db.prepare("SELECT COUNT(*) AS c FROM tasks WHERE status = 'in_progress'").get().c;
|
|
10
|
+
const blockedCount = db.prepare("SELECT COUNT(*) AS c FROM tasks WHERE status = 'blocked'").get().c;
|
|
11
|
+
const unreadNotifs = db.prepare('SELECT COUNT(*) AS c FROM notifications WHERE read = 0').get().c;
|
|
12
|
+
const instructions = {
|
|
13
|
+
role: 'You are connected to TaskFlow, a local-first task and time tracking system. You have access to tools for managing projects, tasks, timers, analytics, activity logs, notifications, and settings.',
|
|
14
|
+
startup_checklist: [
|
|
15
|
+
'Call list_projects to see all projects and their task counts.',
|
|
16
|
+
'Identify which project is relevant to the current work. Use search_projects by name if unsure. If multiple projects match, ask the user to confirm which one before proceeding.',
|
|
17
|
+
'Call list_tasks with status "in_progress" to see what is actively being worked on.',
|
|
18
|
+
'Call list_tasks with status "blocked" to see what is stuck and might need your help.',
|
|
19
|
+
'Call list_notifications with unread_only=true to check for pending notifications.',
|
|
20
|
+
],
|
|
21
|
+
current_state: {
|
|
22
|
+
projects: projectCount,
|
|
23
|
+
total_tasks: taskCount,
|
|
24
|
+
in_progress: inProgressCount,
|
|
25
|
+
blocked: blockedCount,
|
|
26
|
+
unread_notifications: unreadNotifs,
|
|
27
|
+
},
|
|
28
|
+
behavioral_rules: [
|
|
29
|
+
'At the start of a conversation, search for the current project by name using search_projects. Try multiple name variants if needed. If multiple projects match or you are unsure, ask the user to confirm which project to work on before creating or updating tasks. Always link new tasks to the confirmed project.',
|
|
30
|
+
'Before starting any coding work, check if a matching task exists in TaskFlow. If one exists, call start_timer on it to track time.',
|
|
31
|
+
'When you finish a piece of work, call stop_timer with the appropriate final_status: "done" if complete, "partial_done" if more work remains, "blocked" if you hit a blocker.',
|
|
32
|
+
'If you pause to wait for user input or switch context, call pause_timer on the active task.',
|
|
33
|
+
'If you encounter a blocker (missing dependency, unclear requirement, failing test you cannot fix), call stop_timer with final_status "blocked". This closes the active session and marks the task as blocked in one step. Update the task description to explain what is blocking it.',
|
|
34
|
+
'Before starting work on a task, check its dependencies array. If any dependency task is still "not_started" or "in_progress", set the task to "blocked" — do not start working on it until its dependencies are complete.',
|
|
35
|
+
'When the user asks "what should I work on?" or seems unsure what to do next, call list_tasks filtered by priority "critical" or "high" and status "not_started" to surface the most important unblocked work.',
|
|
36
|
+
'After completing a task, check if any blocked tasks had a dependency on it and might now be unblocked.',
|
|
37
|
+
'Periodically check list_notifications with unread_only=true and surface important ones to the user.',
|
|
38
|
+
'Before creating a new task, call list_tasks (or search_tasks) to check if a similar task already exists. Avoid creating duplicates — if a matching task exists, update it or start working on it instead of creating a new one.',
|
|
39
|
+
'When you finish work on a task, you MUST mark it as done. Call stop_timer with final_status "done" — this closes the session and sets the task to done in one step. Never leave a completed task in "in_progress" or "paused" status.',
|
|
40
|
+
'When you create new work items (files, features, fixes), create corresponding tasks in TaskFlow to keep the tracker in sync.',
|
|
41
|
+
'Proactively create tasks on the fly for any work you are doing — bug fixes, improvements, feature implementations, refactors. Create the task, start a timer, do the work, then stop the timer with the final status. Every meaningful unit of work should be tracked, even if the user did not explicitly ask you to create a task for it.',
|
|
42
|
+
'Use Markdown in description fields — headings, bullet lists, code blocks, bold/italic. Task and project descriptions render Markdown in the UI, so well-formatted descriptions are more readable for the user.',
|
|
43
|
+
'When debugging, use the log_debug tool to record your process — what you are investigating, what you tried, error messages, hypotheses, and findings. Link it to the relevant task with task_id. This creates a visible breadcrumb trail in the Activity Pulse that helps the user understand your reasoning.',
|
|
44
|
+
],
|
|
45
|
+
task_workflow: {
|
|
46
|
+
description: 'The standard lifecycle of a task through the system',
|
|
47
|
+
flow: 'not_started → in_progress (start_timer) → paused (pause_timer) → in_progress (start_timer) → done/partial_done/blocked (stop_timer)',
|
|
48
|
+
valid_transitions: {
|
|
49
|
+
not_started: ['in_progress', 'blocked'],
|
|
50
|
+
in_progress: ['paused', 'blocked', 'partial_done', 'done'],
|
|
51
|
+
paused: ['in_progress', 'blocked', 'partial_done', 'done'],
|
|
52
|
+
blocked: ['not_started', 'in_progress'],
|
|
53
|
+
partial_done: ['in_progress', 'done'],
|
|
54
|
+
done: ['in_progress'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
tips: [
|
|
58
|
+
'Tasks have tags — use them to find related work (e.g. list_tasks with tag "bug" or "frontend").',
|
|
59
|
+
'Tasks can have dependencies — check the dependencies array to understand task ordering.',
|
|
60
|
+
'Use get_analytics for a high-level overview of time spent and task completion rates.',
|
|
61
|
+
'Use search_tasks to find tasks by keyword when you are not sure of the exact task ID.',
|
|
62
|
+
'Use search_projects to find a project by name. Try the repo name, directory name, or common abbreviations.',
|
|
63
|
+
'Read task descriptions carefully — they often contain implementation details, acceptance criteria, or context that will help you do better work.',
|
|
64
|
+
'Write task descriptions in Markdown: use ## headings for sections, - bullet lists for steps, ```code blocks``` for snippets, and **bold** for emphasis. The UI renders Markdown natively.',
|
|
65
|
+
'When creating tasks that depend on others, set dependencies and use the "blocked" status to indicate the blocking relationship. This shows up in the dependency graph.',
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
return successResponse(instructions);
|
|
69
|
+
}
|
|
70
|
+
export async function clearData() {
|
|
71
|
+
const db = getDb();
|
|
72
|
+
db.exec('DELETE FROM sessions');
|
|
73
|
+
db.exec('DELETE FROM tasks');
|
|
74
|
+
db.exec('DELETE FROM projects');
|
|
75
|
+
db.exec('DELETE FROM notifications');
|
|
76
|
+
db.exec('DELETE FROM activity_logs');
|
|
77
|
+
logActivity('data_cleared', 'All data cleared', { entityType: 'system' });
|
|
78
|
+
broadcastChange('system', 'data_cleared', {});
|
|
79
|
+
return successResponse({ cleared: true, message: 'All tasks, projects, sessions, notifications, and activity logs deleted. Settings preserved.' });
|
|
80
|
+
}
|
|
81
|
+
// ─── MCP registration ─────────────────────────────────────────────────
|
|
82
|
+
export function registerAgentTools(server) {
|
|
83
|
+
server.tool('get_agent_instructions', '**Call this at the start of every conversation.** Returns onboarding instructions, behavioral rules, and live project context for AI agents working with TaskFlow. This tool tells you how to proactively manage tasks, track time, and stay in sync with the project.', {}, async () => getAgentInstructions());
|
|
84
|
+
server.tool('clear_data', 'Delete ALL tasks, projects, sessions, notifications, and activity logs. Settings are preserved. Use with extreme caution — this is irreversible.', {}, async () => clearData());
|
|
85
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export declare function getAnalytics(params: {
|
|
3
|
+
start_date?: string;
|
|
4
|
+
end_date?: string;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
}>;
|
|
11
|
+
export declare function getTimeline(params: {
|
|
12
|
+
start_date?: string;
|
|
13
|
+
end_date?: string;
|
|
14
|
+
group_by?: 'day' | 'week';
|
|
15
|
+
}): Promise<{
|
|
16
|
+
content: {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
}>;
|
|
21
|
+
export declare function registerAnalyticsTools(server: McpServer): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getDb } from '../db.js';
|
|
3
|
+
import { successResponse } from '../helpers.js';
|
|
4
|
+
// ─── exported handler functions ───────────────────────────────────────
|
|
5
|
+
export async function getAnalytics(params) {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
const { start_date, end_date } = params;
|
|
8
|
+
// Build date filter for sessions
|
|
9
|
+
const sessionConditions = [];
|
|
10
|
+
const sessionValues = [];
|
|
11
|
+
if (start_date) {
|
|
12
|
+
sessionConditions.push('start >= ?');
|
|
13
|
+
sessionValues.push(start_date);
|
|
14
|
+
}
|
|
15
|
+
if (end_date) {
|
|
16
|
+
sessionConditions.push('start <= ?');
|
|
17
|
+
sessionValues.push(end_date);
|
|
18
|
+
}
|
|
19
|
+
const sessionWhere = sessionConditions.length > 0
|
|
20
|
+
? `WHERE ${sessionConditions.join(' AND ')}`
|
|
21
|
+
: '';
|
|
22
|
+
// Total focused time (sum of all session durations in ms)
|
|
23
|
+
const durationFormula = `(julianday(COALESCE(end, datetime('now'))) - julianday(start)) * 86400000`;
|
|
24
|
+
const timeRow = db.prepare(`SELECT COALESCE(SUM(${durationFormula}), 0) AS total_focused_time FROM sessions ${sessionWhere}`).get(...sessionValues);
|
|
25
|
+
const total_focused_time = Math.round(timeRow.total_focused_time);
|
|
26
|
+
// Task counts by status
|
|
27
|
+
const taskRows = db.prepare('SELECT status, COUNT(*) AS count FROM tasks GROUP BY status').all();
|
|
28
|
+
const status_distribution = {};
|
|
29
|
+
let tasks_completed = 0;
|
|
30
|
+
let tasks_in_progress = 0;
|
|
31
|
+
let total_tasks = 0;
|
|
32
|
+
for (const row of taskRows) {
|
|
33
|
+
status_distribution[row.status] = row.count;
|
|
34
|
+
total_tasks += row.count;
|
|
35
|
+
if (row.status === 'done')
|
|
36
|
+
tasks_completed = row.count;
|
|
37
|
+
if (row.status === 'in_progress')
|
|
38
|
+
tasks_in_progress = row.count;
|
|
39
|
+
}
|
|
40
|
+
// Time per project (only tasks that belong to a project)
|
|
41
|
+
const projectTimeRows = db.prepare(`SELECT
|
|
42
|
+
t.project_id,
|
|
43
|
+
p.name AS project_name,
|
|
44
|
+
COALESCE(SUM(${durationFormula.replace(/start/g, 's.start').replace(/end/g, 's.end')}), 0) AS total_time
|
|
45
|
+
FROM sessions s
|
|
46
|
+
JOIN tasks t ON s.task_id = t.id
|
|
47
|
+
JOIN projects p ON t.project_id = p.id
|
|
48
|
+
${sessionWhere.replace(/start/g, 's.start')}
|
|
49
|
+
GROUP BY t.project_id, p.name`).all(...sessionValues);
|
|
50
|
+
const time_per_project = projectTimeRows.map(row => ({
|
|
51
|
+
project_id: row.project_id,
|
|
52
|
+
project_name: row.project_name,
|
|
53
|
+
total_time: Math.round(row.total_time),
|
|
54
|
+
}));
|
|
55
|
+
return successResponse({
|
|
56
|
+
total_focused_time,
|
|
57
|
+
tasks_completed,
|
|
58
|
+
tasks_in_progress,
|
|
59
|
+
total_tasks,
|
|
60
|
+
status_distribution,
|
|
61
|
+
time_per_project,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export async function getTimeline(params) {
|
|
65
|
+
const db = getDb();
|
|
66
|
+
const { start_date, end_date, group_by = 'day' } = params;
|
|
67
|
+
const periodExpr = group_by === 'week'
|
|
68
|
+
? `strftime('%Y-W%W', start)`
|
|
69
|
+
: `strftime('%Y-%m-%d', start)`;
|
|
70
|
+
const durationFormula = `(julianday(COALESCE(end, datetime('now'))) - julianday(start)) * 86400000`;
|
|
71
|
+
const conditions = [];
|
|
72
|
+
const values = [];
|
|
73
|
+
if (start_date) {
|
|
74
|
+
conditions.push('start >= ?');
|
|
75
|
+
values.push(start_date);
|
|
76
|
+
}
|
|
77
|
+
if (end_date) {
|
|
78
|
+
conditions.push('start <= ?');
|
|
79
|
+
values.push(end_date);
|
|
80
|
+
}
|
|
81
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
82
|
+
const rows = db.prepare(`SELECT
|
|
83
|
+
${periodExpr} AS period,
|
|
84
|
+
COALESCE(SUM(${durationFormula}), 0) AS total_time,
|
|
85
|
+
COUNT(*) AS session_count
|
|
86
|
+
FROM sessions
|
|
87
|
+
${where}
|
|
88
|
+
GROUP BY period
|
|
89
|
+
ORDER BY period ASC`).all(...values);
|
|
90
|
+
const timeline = rows.map(row => ({
|
|
91
|
+
period: row.period,
|
|
92
|
+
total_time: Math.round(row.total_time),
|
|
93
|
+
session_count: row.session_count,
|
|
94
|
+
}));
|
|
95
|
+
return successResponse(timeline);
|
|
96
|
+
}
|
|
97
|
+
// ─── MCP registration ─────────────────────────────────────────────────
|
|
98
|
+
export function registerAnalyticsTools(server) {
|
|
99
|
+
server.tool('get_analytics', 'Get a high-level analytics summary: total focused time, task completion rates, status distribution, and time per project. Useful for standup reports or understanding workload.', {
|
|
100
|
+
start_date: z.string().optional(),
|
|
101
|
+
end_date: z.string().optional(),
|
|
102
|
+
}, async (params) => getAnalytics(params));
|
|
103
|
+
server.tool('get_timeline', 'Get focused time grouped by day or week. Use for visualizing work patterns over time.', {
|
|
104
|
+
start_date: z.string().optional(),
|
|
105
|
+
end_date: z.string().optional(),
|
|
106
|
+
group_by: z.enum(['day', 'week']).optional(),
|
|
107
|
+
}, async (params) => getTimeline(params));
|
|
108
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export declare function listNotifications(params: {
|
|
3
|
+
limit?: number;
|
|
4
|
+
unread_only?: boolean;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
}>;
|
|
11
|
+
export declare function markNotificationRead(params: {
|
|
12
|
+
id: number;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
content: {
|
|
15
|
+
type: "text";
|
|
16
|
+
text: string;
|
|
17
|
+
}[];
|
|
18
|
+
}>;
|
|
19
|
+
export declare function markAllNotificationsRead(): Promise<{
|
|
20
|
+
content: {
|
|
21
|
+
type: "text";
|
|
22
|
+
text: string;
|
|
23
|
+
}[];
|
|
24
|
+
}>;
|
|
25
|
+
export declare function clearNotifications(): Promise<{
|
|
26
|
+
content: {
|
|
27
|
+
type: "text";
|
|
28
|
+
text: string;
|
|
29
|
+
}[];
|
|
30
|
+
}>;
|
|
31
|
+
export declare function registerNotificationTools(server: McpServer): void;
|