@dalmasonto/taskflow-mcp 1.0.21 → 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 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
- name: 'taskflow',
65
- version: '1.0.0',
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 () => {
@@ -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
  }
@@ -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.21",
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": "latest",
39
+ "@modelcontextprotocol/sdk": "^1.29.0",
40
40
  "better-sqlite3": "latest",
41
41
  "dotenv": "^17.4.1",
42
42
  "zod": "latest"