@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 +2 -2
- package/dist/sse.d.ts +4 -2
- package/dist/sse.js +78 -12
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
|
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:${
|
|
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
|
+
}
|