@dalmasonto/taskflow-mcp 1.0.20 → 1.0.23
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/dist/db.js +6 -0
- package/dist/index.js +9 -3
- package/dist/resources.js +13 -0
- package/dist/sse.js +6 -7
- package/dist/tools/agent-inbox.js +78 -0
- package/dist/tools/agent.js +51 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -1
- package/package.json +2 -2
package/dist/db.js
CHANGED
|
@@ -105,6 +105,7 @@ function initSchema(db) {
|
|
|
105
105
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
106
106
|
source TEXT NOT NULL DEFAULT 'mcp',
|
|
107
107
|
created_at TEXT NOT NULL,
|
|
108
|
+
broadcast_id TEXT,
|
|
108
109
|
answered_at TEXT
|
|
109
110
|
);
|
|
110
111
|
|
|
@@ -129,6 +130,7 @@ function initSchema(db) {
|
|
|
129
130
|
CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at);
|
|
130
131
|
CREATE INDEX IF NOT EXISTS idx_agent_messages_status ON agent_messages(status);
|
|
131
132
|
CREATE INDEX IF NOT EXISTS idx_agent_messages_project_id ON agent_messages(project_id);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_agent_messages_broadcast_id ON agent_messages(broadcast_id);
|
|
132
134
|
CREATE INDEX IF NOT EXISTS idx_agent_registry_status ON agent_registry(status);
|
|
133
135
|
CREATE INDEX IF NOT EXISTS idx_agent_registry_name ON agent_registry(name);
|
|
134
136
|
CREATE INDEX IF NOT EXISTS idx_agent_registry_project_path ON agent_registry(project_path);
|
|
@@ -162,6 +164,10 @@ function initSchema(db) {
|
|
|
162
164
|
if (!colNames.has('source')) {
|
|
163
165
|
db.exec("ALTER TABLE agent_messages ADD COLUMN source TEXT NOT NULL DEFAULT 'mcp'");
|
|
164
166
|
}
|
|
167
|
+
if (!colNames.has('broadcast_id')) {
|
|
168
|
+
db.exec('ALTER TABLE agent_messages ADD COLUMN broadcast_id TEXT');
|
|
169
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_agent_messages_broadcast_id ON agent_messages(broadcast_id)');
|
|
170
|
+
}
|
|
165
171
|
// Migration: make agent_messages.project_id nullable if it was NOT NULL
|
|
166
172
|
try {
|
|
167
173
|
const tableInfo = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'agent_messages'").get();
|
package/dist/index.js
CHANGED
|
@@ -60,9 +60,15 @@ if (!httpOnly) {
|
|
|
60
60
|
const { registerTerminalTools } = await import('./tools/terminal.js');
|
|
61
61
|
const { registerResources } = await import('./resources.js');
|
|
62
62
|
const { registerCheckpointTools } = await import('./tools/checkpoint.js');
|
|
63
|
-
const server = new McpServer({
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const server = new McpServer({ name: 'taskflow', version: '1.0.23' }, {
|
|
64
|
+
instructions: [
|
|
65
|
+
'You MUST call bootstrap (preferred) or get_agent_instructions before doing any work. This loads your task management rules and project context.',
|
|
66
|
+
'You MUST create a task in TaskFlow before editing files or running commands. search_tasks first to avoid duplicates.',
|
|
67
|
+
'You MUST call start_timer when beginning work and stop_timer when done. Timer lifecycle: start_timer → work → stop_timer(final_status).',
|
|
68
|
+
'Never silently fix bugs — always create or reopen a task first, then track the fix.',
|
|
69
|
+
'When you need user input, call ask_user to post it to the Agent Inbox so the user can respond remotely.',
|
|
70
|
+
'Use log_debug to document your work — it is shared memory visible to the user and other agents.',
|
|
71
|
+
].join('\n'),
|
|
66
72
|
});
|
|
67
73
|
// Wrap server.tool() to track execution time and log failures
|
|
68
74
|
const originalTool = server.tool.bind(server);
|
package/dist/resources.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { getDb } from './db.js';
|
|
3
|
+
import { getAgentInstructions } from './tools/agent.js';
|
|
3
4
|
export function registerResources(server) {
|
|
5
|
+
// ─── Agent Instructions Resource ────────────────────────────────────
|
|
6
|
+
// Auto-loaded by MCP clients on connection — ensures rules are always present
|
|
7
|
+
server.resource('agent-instructions', 'taskflow://instructions', { description: 'TaskFlow agent onboarding instructions — rules, workflow, and live project context', mimeType: 'application/json' }, async (uri) => {
|
|
8
|
+
const instructions = await getAgentInstructions();
|
|
9
|
+
return {
|
|
10
|
+
contents: [{
|
|
11
|
+
uri: uri.href,
|
|
12
|
+
text: JSON.stringify(instructions, null, 2),
|
|
13
|
+
mimeType: 'application/json',
|
|
14
|
+
}],
|
|
15
|
+
};
|
|
16
|
+
});
|
|
4
17
|
// ─── Projects Resource ──────────────────────────────────────────────
|
|
5
18
|
server.resource('projects', new ResourceTemplate('taskflow://projects/{id}', {
|
|
6
19
|
list: async () => {
|
package/dist/sse.js
CHANGED
|
@@ -666,17 +666,16 @@ export async function startSSEServer() {
|
|
|
666
666
|
// Relay command polling + state pushing (if relay is configured)
|
|
667
667
|
if (RELAY_URL && RELAY_PUSH_TOKEN) {
|
|
668
668
|
console.log(`[relay] command polling started → ${RELAY_URL}`);
|
|
669
|
-
// Push state
|
|
669
|
+
// Push lightweight sync state — only what the remote app needs
|
|
670
|
+
// SSE events handle real-time updates after this initial load
|
|
670
671
|
const pushState = () => {
|
|
671
672
|
try {
|
|
672
673
|
const db = getDb();
|
|
673
674
|
const state = {
|
|
674
|
-
tasks: db.prepare('SELECT * FROM tasks').all(),
|
|
675
|
-
projects: db.prepare('SELECT * FROM projects').all(),
|
|
676
|
-
sessions: db.prepare('SELECT * FROM sessions').all(),
|
|
677
|
-
activityLogs: db.prepare('SELECT * FROM activity_logs ORDER BY id DESC LIMIT 100').all(),
|
|
678
|
-
agentMessages: db.prepare('SELECT * FROM agent_messages ORDER BY id DESC LIMIT 200').all(),
|
|
679
675
|
agentRegistry: db.prepare('SELECT * FROM agent_registry').all(),
|
|
676
|
+
agentMessages: db.prepare('SELECT * FROM agent_messages ORDER BY id DESC LIMIT 100').all(),
|
|
677
|
+
notifications: db.prepare('SELECT * FROM notifications ORDER BY id DESC LIMIT 50').all(),
|
|
678
|
+
projects: db.prepare('SELECT * FROM projects').all(),
|
|
680
679
|
};
|
|
681
680
|
fetch(`${RELAY_URL}/push/state`, {
|
|
682
681
|
method: 'POST',
|
|
@@ -687,7 +686,7 @@ export async function startSSEServer() {
|
|
|
687
686
|
catch { /* ignore */ }
|
|
688
687
|
};
|
|
689
688
|
pushState();
|
|
690
|
-
setInterval(pushState,
|
|
689
|
+
setInterval(pushState, 60_000);
|
|
691
690
|
// Poll for commands every 2s
|
|
692
691
|
setInterval(async () => {
|
|
693
692
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import crypto from 'crypto';
|
|
2
3
|
import { getDb } from '../db.js';
|
|
3
4
|
import { logActivity, errorResponse, successResponse, now, broadcastChange } from '../helpers.js';
|
|
4
5
|
import { registerAgent as doRegister, getAgent, listAgents } from '../agent-registry.js';
|
|
@@ -172,4 +173,81 @@ export function registerAgentInboxTools(server) {
|
|
|
172
173
|
const agents = listAgents(params.status);
|
|
173
174
|
return successResponse(agents);
|
|
174
175
|
});
|
|
176
|
+
server.tool('broadcast_agents', 'Send a question to multiple agents simultaneously. Creates a group message — each agent gets their own copy linked by a shared broadcast ID. Use check_broadcast to see all responses. Optional: omit agents list to broadcast to ALL connected agents.', {
|
|
177
|
+
question: z.string().describe('The question to broadcast'),
|
|
178
|
+
agents: z.array(z.string()).optional().describe('Agent names to send to. If omitted, sends to all connected agents.'),
|
|
179
|
+
context: z.string().optional().describe('Markdown context shown before the question'),
|
|
180
|
+
choices: z.array(z.string()).optional().describe('Optional quick-tap choices'),
|
|
181
|
+
project_id: z.number().optional().describe('Optional project ID to attach the messages to'),
|
|
182
|
+
}, { readOnlyHint: false }, async (params) => {
|
|
183
|
+
const db = getDb();
|
|
184
|
+
const senderName = ensureRegistered();
|
|
185
|
+
// Resolve recipients
|
|
186
|
+
let recipients;
|
|
187
|
+
if (params.agents && params.agents.length > 0) {
|
|
188
|
+
for (const name of params.agents) {
|
|
189
|
+
const agent = getAgent(name);
|
|
190
|
+
if (!agent)
|
|
191
|
+
return errorResponse(`Agent "${name}" not found`, 'NOT_FOUND');
|
|
192
|
+
}
|
|
193
|
+
recipients = params.agents;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const connected = listAgents('connected');
|
|
197
|
+
recipients = connected
|
|
198
|
+
.map((a) => a.name)
|
|
199
|
+
.filter((n) => n !== senderName);
|
|
200
|
+
if (recipients.length === 0)
|
|
201
|
+
return errorResponse('No other connected agents to broadcast to', 'VALIDATION_ERROR');
|
|
202
|
+
}
|
|
203
|
+
if (params.project_id) {
|
|
204
|
+
const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(params.project_id);
|
|
205
|
+
if (!project)
|
|
206
|
+
return errorResponse(`Project ${params.project_id} not found`, 'NOT_FOUND');
|
|
207
|
+
}
|
|
208
|
+
const broadcastId = crypto.randomUUID();
|
|
209
|
+
const ts = now();
|
|
210
|
+
const messageIds = [];
|
|
211
|
+
for (const recipient of recipients) {
|
|
212
|
+
const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, source, status, broadcast_id, created_at)
|
|
213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'mcp', 'pending', ?, ?)`).run(params.project_id ?? null, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, recipient, process.ppid, broadcastId, ts);
|
|
214
|
+
const id = result.lastInsertRowid;
|
|
215
|
+
messageIds.push(id);
|
|
216
|
+
const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
|
|
217
|
+
broadcastChange('agent_message', 'agent_question', msg);
|
|
218
|
+
}
|
|
219
|
+
logActivity('agent_broadcast', `Broadcast to ${recipients.length} agents: ${params.question.slice(0, 80)}`, { entityType: 'agent_message' });
|
|
220
|
+
return successResponse({
|
|
221
|
+
broadcastId,
|
|
222
|
+
recipients,
|
|
223
|
+
messageIds,
|
|
224
|
+
count: recipients.length,
|
|
225
|
+
message: `Broadcast sent to ${recipients.length} agents. Use check_broadcast("${broadcastId}") to see responses.`,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
server.tool('check_broadcast', 'Check the status of a broadcast message — shows which agents have responded and their answers.', {
|
|
229
|
+
broadcast_id: z.string().describe('The broadcast ID returned by broadcast_agents'),
|
|
230
|
+
}, { readOnlyHint: true }, async (params) => {
|
|
231
|
+
const db = getDb();
|
|
232
|
+
const messages = db.prepare('SELECT * FROM agent_messages WHERE broadcast_id = ? ORDER BY created_at ASC').all(params.broadcast_id);
|
|
233
|
+
if (messages.length === 0)
|
|
234
|
+
return errorResponse(`No messages found for broadcast ${params.broadcast_id}`, 'NOT_FOUND');
|
|
235
|
+
const total = messages.length;
|
|
236
|
+
const answered = messages.filter(m => m.status === 'answered').length;
|
|
237
|
+
const pending = messages.filter(m => m.status === 'pending').length;
|
|
238
|
+
return successResponse({
|
|
239
|
+
broadcastId: params.broadcast_id,
|
|
240
|
+
question: messages[0].question,
|
|
241
|
+
total,
|
|
242
|
+
answered,
|
|
243
|
+
pending,
|
|
244
|
+
responses: messages.map(m => ({
|
|
245
|
+
id: m.id,
|
|
246
|
+
recipient: m.recipient_name,
|
|
247
|
+
status: m.status,
|
|
248
|
+
response: m.response ?? null,
|
|
249
|
+
answered_at: m.answered_at ?? null,
|
|
250
|
+
})),
|
|
251
|
+
});
|
|
252
|
+
});
|
|
175
253
|
}
|
package/dist/tools/agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { getDb } from '../db.js';
|
|
2
3
|
import { logActivity, successResponse, broadcastChange } from '../helpers.js';
|
|
3
4
|
// ─── exported handler functions ───────────────────────────────────────
|
|
@@ -93,6 +94,7 @@ export async function getAgentInstructions() {
|
|
|
93
94
|
'list_tasks/search_tasks return compact summaries. Use get_task(id) to read full descriptions.',
|
|
94
95
|
'log_debug accepts task_id OR project_id — use project_id for project-wide notes visible on the project page.',
|
|
95
96
|
'Multi-agent: use register_agent to set your name, list_agents to see who is online, send_to_agent for updates, ask_agent for questions that need answers.',
|
|
97
|
+
'Multi-agent: use broadcast_agents to send a question to multiple agents at once (group chat). Omit the agents list to broadcast to ALL connected agents. Use check_broadcast(broadcast_id) to see all responses and their status.',
|
|
96
98
|
'Multi-agent: task dependencies are the backbone of coordination — use them to enforce build order so agents don\'t step on each other.',
|
|
97
99
|
'Multi-agent: log_debug with project_id is shared memory — other agents read it to understand decisions, gotchas, and architecture context.',
|
|
98
100
|
],
|
|
@@ -113,5 +115,54 @@ export async function clearData() {
|
|
|
113
115
|
// ─── MCP registration ─────────────────────────────────────────────────
|
|
114
116
|
export function registerAgentTools(server) {
|
|
115
117
|
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.', {}, { readOnlyHint: true }, async () => getAgentInstructions());
|
|
118
|
+
server.tool('bootstrap', '**CALL THIS FIRST.** One-shot startup: returns your agent instructions, active project (auto-detected from folder name), open/blocked tasks, unread notifications, and registered agents — all in one call. Replaces the need to call get_agent_instructions + search_projects + list_tasks + list_notifications + list_agents separately.', {
|
|
119
|
+
project_name: z.string().optional().describe('Override project name (default: auto-detected from working directory folder name)'),
|
|
120
|
+
}, { readOnlyHint: true }, async (params) => {
|
|
121
|
+
const db = getDb();
|
|
122
|
+
const { listAgents } = await import('../agent-registry.js');
|
|
123
|
+
// Get instructions
|
|
124
|
+
const instructionsResult = await getAgentInstructions();
|
|
125
|
+
const instructions = instructionsResult.content?.[0]?.text
|
|
126
|
+
? JSON.parse(instructionsResult.content[0].text)
|
|
127
|
+
: null;
|
|
128
|
+
// Auto-detect project from folder name or use override
|
|
129
|
+
const { myAgentName } = await import('./agent-inbox.js');
|
|
130
|
+
const agentEntry = myAgentName
|
|
131
|
+
? db.prepare('SELECT project_path FROM agent_registry WHERE name = ?').get(myAgentName)
|
|
132
|
+
: null;
|
|
133
|
+
const folderName = params.project_name
|
|
134
|
+
?? (agentEntry?.project_path ? agentEntry.project_path.split('/').pop() : null);
|
|
135
|
+
let project = null;
|
|
136
|
+
let tasks = [];
|
|
137
|
+
if (folderName) {
|
|
138
|
+
const projects = db.prepare('SELECT * FROM projects WHERE LOWER(name) LIKE ?').all(`%${folderName.toLowerCase()}%`);
|
|
139
|
+
if (projects.length === 1) {
|
|
140
|
+
project = projects[0];
|
|
141
|
+
tasks = db.prepare("SELECT id, title, status, priority, tags, estimated_time FROM tasks WHERE project_id = ? AND status IN ('not_started', 'in_progress', 'paused', 'blocked') ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END").all(project.id);
|
|
142
|
+
}
|
|
143
|
+
else if (projects.length > 1) {
|
|
144
|
+
project = { _ambiguous: true, matches: projects.map(p => ({ id: p.id, name: p.name })), message: 'Multiple projects match — ask the user which one to use.' };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Unread notifications
|
|
148
|
+
const notifications = db.prepare("SELECT * FROM notifications WHERE read = 0 ORDER BY created_at DESC LIMIT 10").all();
|
|
149
|
+
// Active agents
|
|
150
|
+
const agents = listAgents('connected');
|
|
151
|
+
// Pending inbox messages for this agent
|
|
152
|
+
const agentName = myAgentName ?? 'unknown';
|
|
153
|
+
const pendingMessages = db.prepare("SELECT id, sender_name, question, created_at FROM agent_messages WHERE recipient_name = ? AND status = 'pending' ORDER BY created_at ASC").all(agentName);
|
|
154
|
+
return successResponse({
|
|
155
|
+
instructions,
|
|
156
|
+
project,
|
|
157
|
+
tasks,
|
|
158
|
+
taskCount: tasks.length,
|
|
159
|
+
notifications,
|
|
160
|
+
unreadCount: notifications.length,
|
|
161
|
+
agents,
|
|
162
|
+
pendingMessages,
|
|
163
|
+
pendingMessageCount: pendingMessages.length,
|
|
164
|
+
agentName,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
116
167
|
server.tool('clear_data', 'Delete ALL tasks, projects, sessions, notifications, and activity logs. Settings are preserved. Use with extreme caution — this is irreversible.', {}, { destructiveHint: true }, async () => clearData());
|
|
117
168
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ export declare const ActivityAction: z.ZodEnum<{
|
|
|
64
64
|
debug_log: "debug_log";
|
|
65
65
|
agent_question: "agent_question";
|
|
66
66
|
agent_question_answered: "agent_question_answered";
|
|
67
|
+
agent_broadcast: "agent_broadcast";
|
|
67
68
|
agent_connected: "agent_connected";
|
|
68
69
|
agent_disconnected: "agent_disconnected";
|
|
69
70
|
terminal_send_keys: "terminal_send_keys";
|
package/dist/types.js
CHANGED
|
@@ -14,7 +14,7 @@ export const ActivityAction = z.enum([
|
|
|
14
14
|
'tasks_bulk_created', 'settings_saved', 'data_seeded', 'data_cleared',
|
|
15
15
|
'task_linked', 'task_unlinked', 'dependency_added', 'dependency_removed',
|
|
16
16
|
'link_added', 'tag_added', 'tag_removed', 'debug_log',
|
|
17
|
-
'agent_question', 'agent_question_answered',
|
|
17
|
+
'agent_question', 'agent_question_answered', 'agent_broadcast',
|
|
18
18
|
'agent_connected', 'agent_disconnected',
|
|
19
19
|
'terminal_send_keys', 'terminal_captured',
|
|
20
20
|
'compaction_summary', 'activity_compacted',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dalmasonto/taskflow-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"description": "MCP server for TaskFlow — manage projects, tasks, timers, analytics via AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"test:watch": "vitest"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/sdk": "
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
40
|
"better-sqlite3": "latest",
|
|
41
41
|
"dotenv": "^17.4.1",
|
|
42
42
|
"zod": "latest"
|