@dalmasonto/taskflow-mcp 1.0.4 → 1.0.6

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/index.js CHANGED
@@ -31,8 +31,8 @@ function cleanupOrphanedSessions() {
31
31
  }
32
32
  }
33
33
  cleanupOrphanedSessions();
34
- // Always start the HTTP/SSE server
35
- startSSEServer();
34
+ // Always start the HTTP/SSE server (probes for existing instances, finds fallback port)
35
+ await startSSEServer();
36
36
  // Only start MCP stdio transport when not in http-only mode
37
37
  if (!httpOnly) {
38
38
  const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
package/dist/sse.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- export declare function startSSEServer(): void;
1
+ export declare function startSSEServer(): Promise<void>;
2
2
  export declare function markSSEActive(): void;
3
3
  /**
4
4
  * Broadcast an SSE event. If this process owns the SSE server, send directly.
5
- * Otherwise, relay via HTTP to the process that does (sidecar on port 3456).
5
+ * Otherwise, relay via HTTP to the process that does.
6
6
  */
7
7
  export declare function broadcast(event: string, data: object): void;
8
+ /** Returns the port the SSE server is actively using */
9
+ export declare function getActivePort(): number;
package/dist/sse.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { createServer } from 'http';
2
2
  import { getDb } from './db.js';
3
3
  import { logActivity } from './helpers.js';
4
+ const SERVICE_ID = 'taskflow-mcp';
5
+ const MAX_PORT_ATTEMPTS = 10;
6
+ const PROBE_TIMEOUT_MS = 2000;
4
7
  const clients = new Set();
5
8
  function resolvePort() {
6
9
  // CLI arg takes priority: --port 4000
@@ -14,7 +17,27 @@ function resolvePort() {
14
17
  }
15
18
  return 3456;
16
19
  }
17
- const PORT = resolvePort();
20
+ const PREFERRED_PORT = resolvePort();
21
+ /** The port that is actually serving SSE — either ours or an existing instance's */
22
+ let activePort = PREFERRED_PORT;
23
+ /** Probe a port to check if a TaskFlow service is already running there */
24
+ async function probeTaskFlow(port) {
25
+ const controller = new AbortController();
26
+ const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
27
+ try {
28
+ const res = await fetch(`http://localhost:${port}/healthz`, { signal: controller.signal });
29
+ if (!res.ok)
30
+ return false;
31
+ const body = await res.json();
32
+ return body.service === SERVICE_ID;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ finally {
38
+ clearTimeout(timeout);
39
+ }
40
+ }
18
41
  function jsonResponse(res, status, data) {
19
42
  res.writeHead(status, { 'Content-Type': 'application/json' });
20
43
  res.end(JSON.stringify(data));
@@ -26,7 +49,7 @@ function readBody(req) {
26
49
  req.on('end', () => resolve(body));
27
50
  });
28
51
  }
29
- export function startSSEServer() {
52
+ export async function startSSEServer() {
30
53
  const server = createServer(async (req, res) => {
31
54
  // CORS headers for all requests
32
55
  res.setHeader('Access-Control-Allow-Origin', '*');
@@ -37,6 +60,11 @@ export function startSSEServer() {
37
60
  res.end();
38
61
  return;
39
62
  }
63
+ // GET /healthz — identity probe so other instances can detect us
64
+ if (req.url === '/healthz' && req.method === 'GET') {
65
+ jsonResponse(res, 200, { service: SERVICE_ID, pid: process.pid });
66
+ return;
67
+ }
40
68
  if (req.url === '/events' && req.method === 'GET') {
41
69
  res.writeHead(200, {
42
70
  'Content-Type': 'text/event-stream',
@@ -237,15 +265,49 @@ export function startSSEServer() {
237
265
  res.writeHead(404);
238
266
  res.end('Not Found');
239
267
  });
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;
268
+ // Try ports sequentially: probe occupied ports to see if they're ours
269
+ await new Promise((resolve) => {
270
+ let attempt = 0;
271
+ function tryPort(port) {
272
+ if (attempt >= MAX_PORT_ATTEMPTS) {
273
+ console.error(`[SSE] failed to bind after ${MAX_PORT_ATTEMPTS} attempts (ports ${PREFERRED_PORT}–${port - 1})`);
274
+ resolve();
275
+ return;
276
+ }
277
+ server.once('error', async (err) => {
278
+ if (err.code === 'EADDRINUSE') {
279
+ const isTaskFlow = await probeTaskFlow(port);
280
+ if (isTaskFlow) {
281
+ // Another TaskFlow instance owns this port — connect to it instead
282
+ activePort = port;
283
+ console.log(`[SSE] port ${port} owned by another TaskFlow instance (pid probe) — using it`);
284
+ resolve();
285
+ }
286
+ else {
287
+ // Not ours — try next port
288
+ console.log(`[SSE] port ${port} in use by non-TaskFlow service — trying ${port + 1}`);
289
+ attempt++;
290
+ tryPort(port + 1);
291
+ }
292
+ }
293
+ else {
294
+ console.error('[SSE] unexpected server error:', err.message);
295
+ resolve();
296
+ }
297
+ });
298
+ server.listen(port, '0.0.0.0', () => {
299
+ activePort = port;
300
+ markSSEActive();
301
+ if (port !== PREFERRED_PORT) {
302
+ console.log(`[SSE] listening on fallback port ${port} (preferred ${PREFERRED_PORT} was unavailable)`);
303
+ }
304
+ else {
305
+ console.log(`[SSE] listening on port ${port}`);
306
+ }
307
+ resolve();
308
+ });
244
309
  }
245
- // Unexpected error — still don't crash the MCP process
246
- });
247
- server.listen(PORT, '0.0.0.0', () => {
248
- markSSEActive();
310
+ tryPort(PREFERRED_PORT);
249
311
  });
250
312
  }
251
313
  /** Broadcast directly to connected SSE clients in this process */
@@ -262,7 +324,7 @@ export function markSSEActive() {
262
324
  }
263
325
  /**
264
326
  * 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).
327
+ * Otherwise, relay via HTTP to the process that does.
266
328
  */
267
329
  export function broadcast(event, data) {
268
330
  if (sseServerActive && clients.size > 0) {
@@ -271,7 +333,7 @@ export function broadcast(event, data) {
271
333
  else {
272
334
  // Relay to the SSE server owner via HTTP
273
335
  const body = JSON.stringify({ event, data });
274
- fetch(`http://localhost:${PORT}/api/broadcast`, {
336
+ fetch(`http://localhost:${activePort}/api/broadcast`, {
275
337
  method: 'POST',
276
338
  headers: { 'Content-Type': 'application/json' },
277
339
  body,
@@ -280,3 +342,7 @@ export function broadcast(event, data) {
280
342
  });
281
343
  }
282
344
  }
345
+ /** Returns the port the SSE server is actively using */
346
+ export function getActivePort() {
347
+ return activePort;
348
+ }
@@ -25,16 +25,17 @@ export async function getAgentInstructions() {
25
25
  },
26
26
  rules: [
27
27
  // Task tracking
28
- 'Proactively create tasks for ALL substantial work (features, bugs, refactors). search_tasks first to avoid duplicates. Link tasks to the confirmed project.',
29
- 'Timer lifecycle: start_timer → work → stop_timer(final_status). Use "done"/"partial_done"/"blocked". pause_timer when waiting for input.',
30
- 'MUST stop_timer with "done" when work is complete. Never leave finished tasks in "in_progress" or "paused".',
28
+ 'Proactively create tasks for ALL substantial work features, bugs, refactors, AND debugging/investigation. Debugging is real work: create a task for it (e.g. "Debug: SSE connection dropping"), start a timer, and track it the same way you would a feature. search_tasks first to avoid duplicates. Link tasks to the confirmed project.',
29
+ 'Timer lifecycle: start_timer → work → stop_timer(final_status). Use "done"/"partial_done"/"blocked". pause_timer when waiting for input. This applies equally to debugging tasks — start a timer before investigating, stop it when resolved or blocked.',
30
+ 'MUST stop_timer with "done" when work is complete. Never leave finished tasks in "in_progress" or "paused". This includes debugging tasks — when the bug is fixed or the investigation concludes, stop the timer.',
31
31
  // Dependencies
32
32
  'Check task dependencies before starting. If any dep is incomplete, set task to "blocked".',
33
33
  'After completing a task, check if blocked tasks depending on it can be unblocked.',
34
34
  // Prioritization
35
35
  'When user is unsure what to work on: list_tasks priority="critical"/"high" status="not_started".',
36
36
  // Logging
37
- 'Use log_debug with task_id to record debugging steps, hypotheses, findings, decisions. Use Markdown formatting.',
37
+ 'ALWAYS log your entire debugging process using log_debug with task_id. Log every stage: issue identification, hypothesis, files read/edited, commands run, errors encountered, fixes attempted, and resolution. This creates a visible trail in the Activity Pulse so users can follow your reasoning in real-time.',
38
+ 'log_debug early and often — not just at the end. Log when you start investigating, when you find a clue, when you edit a file, and when the fix lands. Use Markdown: headings for stages, code blocks for errors/paths, bold for key findings.',
38
39
  // Formatting
39
40
  'Use Markdown in descriptions — headings, bullets, code blocks, bold. The UI renders it.',
40
41
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",