@dalmasonto/taskflow-mcp 1.0.5 → 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.5",
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",