workmatic 1.1.3 → 1.2.0

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/cli.js CHANGED
@@ -22,6 +22,11 @@ function createDatabase(options = {}) {
22
22
  sqliteDb.pragma("journal_mode = WAL");
23
23
  sqliteDb.pragma("synchronous = NORMAL");
24
24
  sqliteDb.pragma("busy_timeout = 5000");
25
+ sqliteDb.pragma("cache_size = -64000");
26
+ sqliteDb.pragma("temp_store = MEMORY");
27
+ sqliteDb.pragma("mmap_size = 268435456");
28
+ const cacheSize = options.statementCacheSize ?? 1e3;
29
+ enableStatementCache(sqliteDb, cacheSize);
25
30
  const db = new Kysely({
26
31
  dialect: new SqliteDialect({
27
32
  database: sqliteDb
@@ -49,13 +54,23 @@ function createSchema(db) {
49
54
  last_error TEXT
50
55
  )
51
56
  `);
57
+ ensurePartialIndex(
58
+ db,
59
+ "idx_workmatic_jobs_claim",
60
+ `CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_claim
61
+ ON workmatic_jobs (queue, status, run_at, priority, id)
62
+ WHERE status = 'ready'`
63
+ );
64
+ ensurePartialIndex(
65
+ db,
66
+ "idx_workmatic_jobs_lease",
67
+ `CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_lease
68
+ ON workmatic_jobs (status, lease_until)
69
+ WHERE status = 'running'`
70
+ );
52
71
  db.exec(`
53
- CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_claim
54
- ON workmatic_jobs (queue, status, run_at, priority, id)
55
- `);
56
- db.exec(`
57
- CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_lease
58
- ON workmatic_jobs (status, lease_until)
72
+ CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_queue_status
73
+ ON workmatic_jobs (queue, status)
59
74
  `);
60
75
  db.exec(`
61
76
  CREATE TABLE IF NOT EXISTS workmatic_settings (
@@ -68,13 +83,53 @@ function createSchema(db) {
68
83
  UPDATE workmatic_jobs SET status = 'dead' WHERE status = 'failed'
69
84
  `);
70
85
  }
86
+ function ensurePartialIndex(db, indexName, createSql) {
87
+ const row = db.prepare(
88
+ "SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?"
89
+ ).get(indexName);
90
+ if (row?.sql && !row.sql.toUpperCase().includes("WHERE")) {
91
+ db.exec(`DROP INDEX IF EXISTS ${indexName}`);
92
+ }
93
+ db.exec(createSql);
94
+ }
95
+ function enableStatementCache(db, maxStatements = 1e3) {
96
+ if (maxStatements <= 0) {
97
+ return db;
98
+ }
99
+ const originalPrepare = db.prepare.bind(db);
100
+ const cache = /* @__PURE__ */ new Map();
101
+ db.prepare = function(sql4) {
102
+ const cached = cache.get(sql4);
103
+ if (cached) {
104
+ cache.delete(sql4);
105
+ cache.set(sql4, cached);
106
+ if (!cached.busy) {
107
+ return cached;
108
+ }
109
+ return originalPrepare(sql4);
110
+ }
111
+ const stmt = originalPrepare(sql4);
112
+ if (cache.size >= maxStatements) {
113
+ const oldestKey = cache.keys().next().value;
114
+ cache.delete(oldestKey);
115
+ }
116
+ cache.set(sql4, stmt);
117
+ return stmt;
118
+ };
119
+ const originalClose = db.close.bind(db);
120
+ db.close = function() {
121
+ cache.clear();
122
+ return originalClose();
123
+ };
124
+ return db;
125
+ }
71
126
 
72
127
  // src/orchestrator.ts
73
- import { sql as sql3 } from "kysely";
128
+ import { sql as sql2 } from "kysely";
74
129
 
75
130
  // src/client.ts
76
131
  import { nanoid } from "nanoid";
77
- import { sql } from "kysely";
132
+ import { CompiledQuery } from "kysely";
78
133
 
79
134
  // src/utils.ts
80
135
  var defaultBackoff = (attempts) => {
@@ -104,10 +159,16 @@ function now() {
104
159
 
105
160
  // src/client.ts
106
161
  function createClient(options) {
107
- const { db, queue = "default" } = options;
162
+ const { db, queue = "default", onJobAdded, worker } = options;
108
163
  if (!db) {
109
164
  throw new Error("Database instance is required");
110
165
  }
166
+ function notifyJobAdded(delayMs) {
167
+ if (delayMs <= 0) {
168
+ worker?.wakeUp();
169
+ onJobAdded?.();
170
+ }
171
+ }
111
172
  return {
112
173
  /**
113
174
  * Add a job to the queue
@@ -122,20 +183,13 @@ function createClient(options) {
122
183
  const publicId = nanoid();
123
184
  const timestamp = now();
124
185
  const runAt = timestamp + delayMs;
125
- await db.insertInto("workmatic_jobs").values({
126
- public_id: publicId,
127
- queue,
128
- payload: payloadJson,
129
- status: "ready",
130
- priority,
131
- run_at: runAt,
132
- attempts: 0,
133
- max_attempts: maxAttempts,
134
- lease_until: 0,
135
- created_at: timestamp,
136
- updated_at: timestamp,
137
- last_error: null
138
- }).execute();
186
+ await db.executeQuery(
187
+ CompiledQuery.raw(
188
+ `INSERT INTO workmatic_jobs (public_id, queue, payload, status, priority, run_at, attempts, max_attempts, lease_until, created_at, updated_at, last_error) VALUES (?, ?, ?, 'ready', ?, ?, 0, ?, 0, ?, ?, null)`,
189
+ [publicId, queue, payloadJson, priority, runAt, maxAttempts, timestamp, timestamp]
190
+ )
191
+ );
192
+ notifyJobAdded(delayMs);
139
193
  return { ok: true, id: publicId };
140
194
  },
141
195
  async addMany(payloads, opts = {}) {
@@ -149,7 +203,7 @@ function createClient(options) {
149
203
  }
150
204
  const timestamp = now();
151
205
  const runAt = timestamp + delayMs;
152
- return await db.transaction().execute(async (trx) => {
206
+ const result = await db.transaction().execute(async (trx) => {
153
207
  const ids = [];
154
208
  const rows = payloads.map((payload) => {
155
209
  const payloadJson = validatePayload(payload);
@@ -173,15 +227,19 @@ function createClient(options) {
173
227
  await trx.insertInto("workmatic_jobs").values(rows).execute();
174
228
  return { ok: true, ids };
175
229
  });
230
+ notifyJobAdded(delayMs);
231
+ return result;
176
232
  },
177
233
  /**
178
234
  * Get job statistics for the queue
179
235
  */
180
236
  async stats() {
181
- const result = await db.selectFrom("workmatic_jobs").select([
182
- "status",
183
- sql`count(*)`.as("count")
184
- ]).where("queue", "=", queue).groupBy("status").execute();
237
+ const result = await db.executeQuery(
238
+ CompiledQuery.raw(
239
+ "SELECT status, count(*) AS count FROM workmatic_jobs WHERE queue = ? GROUP BY status",
240
+ [queue]
241
+ )
242
+ );
185
243
  const stats = {
186
244
  ready: 0,
187
245
  running: 0,
@@ -189,7 +247,7 @@ function createClient(options) {
189
247
  dead: 0,
190
248
  total: 0
191
249
  };
192
- for (const row of result) {
250
+ for (const row of result.rows) {
193
251
  const status = row.status;
194
252
  const count = Number(row.count);
195
253
  if (status in stats) {
@@ -215,7 +273,77 @@ function createClient(options) {
215
273
 
216
274
  // src/worker.ts
217
275
  import fastq from "fastq";
218
- import { sql as sql2 } from "kysely";
276
+ import { sql, CompiledQuery as CompiledQuery2 } from "kysely";
277
+
278
+ // src/shutdown.ts
279
+ function attachGracefulShutdown(target, options = {}) {
280
+ const {
281
+ signals = ["SIGINT", "SIGTERM"],
282
+ timeoutMs = 3e4,
283
+ exitOnComplete = true,
284
+ exitCode = 0,
285
+ timeoutExitCode = 1,
286
+ onShutdownStart,
287
+ onShutdownComplete,
288
+ onShutdownError
289
+ } = options;
290
+ let shuttingDown = false;
291
+ async function stopTarget() {
292
+ if (Array.isArray(target)) {
293
+ await Promise.all(target.map((w) => w.stop()));
294
+ } else if ("stopAll" in target) {
295
+ await target.stopAll();
296
+ } else {
297
+ await target.stop();
298
+ }
299
+ }
300
+ const handler = async (signal) => {
301
+ if (shuttingDown) {
302
+ return;
303
+ }
304
+ shuttingDown = true;
305
+ onShutdownStart?.(signal);
306
+ let timer = null;
307
+ if (timeoutMs > 0) {
308
+ timer = setTimeout(() => {
309
+ const err = new Error(`Graceful shutdown timed out after ${timeoutMs}ms`);
310
+ onShutdownError?.(err);
311
+ if (exitOnComplete) {
312
+ process.exit(timeoutExitCode);
313
+ }
314
+ }, timeoutMs);
315
+ timer.unref();
316
+ }
317
+ try {
318
+ await stopTarget();
319
+ if (timer) {
320
+ clearTimeout(timer);
321
+ }
322
+ onShutdownComplete?.();
323
+ if (exitOnComplete) {
324
+ process.exit(exitCode);
325
+ }
326
+ } catch (err) {
327
+ if (timer) {
328
+ clearTimeout(timer);
329
+ }
330
+ onShutdownError?.(err);
331
+ if (exitOnComplete) {
332
+ process.exit(timeoutExitCode);
333
+ }
334
+ }
335
+ };
336
+ for (const sig of signals) {
337
+ process.on(sig, handler);
338
+ }
339
+ return function detach() {
340
+ for (const sig of signals) {
341
+ process.removeListener(sig, handler);
342
+ }
343
+ };
344
+ }
345
+
346
+ // src/worker.ts
219
347
  var DEFAULT_WORKER_TIMEOUT_MS = 6e4;
220
348
  function parseClaimedRows(result) {
221
349
  const rows = result.rows;
@@ -253,7 +381,8 @@ function createWorker(options) {
253
381
  autoRestore = true,
254
382
  pauseCheckIntervalMs = 300,
255
383
  requeueExpiredIntervalMs = 0,
256
- onPumpError
384
+ onPumpError,
385
+ completionBatchSize = 50
257
386
  } = options;
258
387
  if (!db) {
259
388
  throw new Error("Database instance is required");
@@ -266,6 +395,8 @@ function createWorker(options) {
266
395
  let lastPauseCheckAt = 0;
267
396
  let cachedDbPaused = false;
268
397
  let lastRequeueAt = 0;
398
+ let pendingDone = [];
399
+ let flushTimeout = null;
269
400
  function notifyPumpError(error) {
270
401
  console.error("[workmatic] Pump error:", error);
271
402
  onPumpError?.(error);
@@ -278,7 +409,7 @@ function createWorker(options) {
278
409
  const timestamp = now();
279
410
  const key = getStateKey();
280
411
  try {
281
- await sql2`
412
+ await sql`
282
413
  INSERT INTO workmatic_settings (queue, paused, updated_at)
283
414
  VALUES (${key}, ${state === "paused" ? 1 : state === "running" ? 2 : 0}, ${timestamp})
284
415
  ON CONFLICT(queue) DO UPDATE SET
@@ -313,52 +444,104 @@ function createWorker(options) {
313
444
  async function claimBatch(limit) {
314
445
  const timestamp = now();
315
446
  const leaseUntil = timestamp + leaseMs;
316
- return await db.transaction().execute(async (trx) => {
317
- const result = await sql2`
318
- UPDATE workmatic_jobs
319
- SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
320
- WHERE rowid IN (
321
- SELECT rowid FROM workmatic_jobs
322
- WHERE queue = ${queue}
323
- AND status = 'ready'
324
- AND run_at <= ${timestamp}
325
- ORDER BY priority ASC, id ASC
326
- LIMIT ${limit}
447
+ const result = await sql`
448
+ UPDATE workmatic_jobs
449
+ SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
450
+ WHERE rowid IN (
451
+ SELECT rowid FROM workmatic_jobs
452
+ WHERE queue = ${queue}
453
+ AND status = 'ready'
454
+ AND run_at <= ${timestamp}
455
+ ORDER BY priority ASC, id ASC
456
+ LIMIT ${limit}
457
+ )
458
+ RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
459
+ `.execute(db);
460
+ return parseClaimedRows(result);
461
+ }
462
+ async function flushDoneBatch() {
463
+ if (flushTimeout !== null) {
464
+ clearImmediate(flushTimeout);
465
+ flushTimeout = null;
466
+ }
467
+ if (pendingDone.length === 0) {
468
+ return;
469
+ }
470
+ const current = pendingDone;
471
+ pendingDone = [];
472
+ const timestamp = now();
473
+ try {
474
+ if (current.length === 1) {
475
+ await db.executeQuery(
476
+ CompiledQuery2.raw(
477
+ "UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id = ?",
478
+ [timestamp, current[0].id]
479
+ )
480
+ );
481
+ } else {
482
+ const CHUNK_SIZE = 500;
483
+ for (let i = 0; i < current.length; i += CHUNK_SIZE) {
484
+ const chunk = current.slice(i, i + CHUNK_SIZE);
485
+ const placeholders = chunk.map(() => "?").join(", ");
486
+ const params = [timestamp, ...chunk.map((item) => item.id)];
487
+ await db.executeQuery(
488
+ CompiledQuery2.raw(
489
+ `UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id IN (${placeholders})`,
490
+ params
491
+ )
492
+ );
493
+ }
494
+ }
495
+ for (const item of current) {
496
+ item.resolve();
497
+ }
498
+ } catch (err) {
499
+ for (const item of current) {
500
+ item.reject(err);
501
+ }
502
+ }
503
+ }
504
+ function markDone(jobId) {
505
+ if (completionBatchSize <= 0) {
506
+ return db.executeQuery(
507
+ CompiledQuery2.raw(
508
+ "UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id = ?",
509
+ [now(), jobId]
327
510
  )
328
- RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
329
- `.execute(trx);
330
- return parseClaimedRows(result);
511
+ ).then(() => {
512
+ });
513
+ }
514
+ return new Promise((resolve, reject) => {
515
+ pendingDone.push({ id: jobId, resolve, reject });
516
+ if (pendingDone.length >= completionBatchSize) {
517
+ void flushDoneBatch();
518
+ } else if (!flushTimeout) {
519
+ flushTimeout = setImmediate(() => {
520
+ flushTimeout = null;
521
+ void flushDoneBatch();
522
+ });
523
+ }
331
524
  });
332
525
  }
333
- async function markDone(jobId) {
334
- await db.updateTable("workmatic_jobs").set({
335
- status: "done",
336
- lease_until: 0,
337
- updated_at: now()
338
- }).where("id", "=", jobId).execute();
339
- }
340
526
  async function markFailed(jobId, attempts, maxAttempts, error) {
341
527
  const timestamp = now();
342
528
  const newAttempts = attempts + 1;
343
529
  const errorMessage = error.message || String(error);
344
530
  if (newAttempts < maxAttempts) {
345
531
  const runAt = timestamp + backoff(newAttempts);
346
- await db.updateTable("workmatic_jobs").set({
347
- status: "ready",
348
- attempts: newAttempts,
349
- run_at: runAt,
350
- lease_until: 0,
351
- last_error: errorMessage,
352
- updated_at: timestamp
353
- }).where("id", "=", jobId).execute();
532
+ await db.executeQuery(
533
+ CompiledQuery2.raw(
534
+ "UPDATE workmatic_jobs SET status = 'ready', attempts = ?, run_at = ?, lease_until = 0, last_error = ?, updated_at = ? WHERE id = ?",
535
+ [newAttempts, runAt, errorMessage, timestamp, jobId]
536
+ )
537
+ );
354
538
  } else {
355
- await db.updateTable("workmatic_jobs").set({
356
- status: "dead",
357
- attempts: newAttempts,
358
- lease_until: 0,
359
- last_error: errorMessage,
360
- updated_at: timestamp
361
- }).where("id", "=", jobId).execute();
539
+ await db.executeQuery(
540
+ CompiledQuery2.raw(
541
+ "UPDATE workmatic_jobs SET status = 'dead', attempts = ?, lease_until = 0, last_error = ?, updated_at = ? WHERE id = ?",
542
+ [newAttempts, errorMessage, timestamp, jobId]
543
+ )
544
+ );
362
545
  }
363
546
  }
364
547
  async function withTimeout(promise, ms, jobId) {
@@ -405,8 +588,13 @@ function createWorker(options) {
405
588
  }
406
589
  }
407
590
  async function isQueuePausedInDb() {
408
- const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", queue).executeTakeFirst();
409
- return setting?.paused === 1;
591
+ const result = await db.executeQuery(
592
+ CompiledQuery2.raw(
593
+ "SELECT paused FROM workmatic_settings WHERE queue = ? LIMIT 1",
594
+ [queue]
595
+ )
596
+ );
597
+ return result.rows[0]?.paused === 1;
410
598
  }
411
599
  async function pump() {
412
600
  if (!running) {
@@ -475,10 +663,12 @@ function createWorker(options) {
475
663
  }
476
664
  await fastqQueue.drained();
477
665
  fastqQueue = null;
666
+ await flushDoneBatch();
478
667
  await saveState("stopped");
479
668
  },
480
669
  pause() {
481
670
  paused = true;
671
+ void flushDoneBatch();
482
672
  void saveState("paused");
483
673
  },
484
674
  resume() {
@@ -486,10 +676,13 @@ function createWorker(options) {
486
676
  void saveState("running");
487
677
  },
488
678
  async stats() {
489
- const result = await db.selectFrom("workmatic_jobs").select([
490
- "status",
491
- sql2`count(*)`.as("count")
492
- ]).where("queue", "=", queue).groupBy("status").execute();
679
+ await flushDoneBatch();
680
+ const result = await db.executeQuery(
681
+ CompiledQuery2.raw(
682
+ "SELECT status, count(*) AS count FROM workmatic_jobs WHERE queue = ? GROUP BY status",
683
+ [queue]
684
+ )
685
+ );
493
686
  const stats = {
494
687
  ready: 0,
495
688
  running: 0,
@@ -497,7 +690,7 @@ function createWorker(options) {
497
690
  dead: 0,
498
691
  total: 0
499
692
  };
500
- for (const row of result) {
693
+ for (const row of result.rows) {
501
694
  const status = row.status;
502
695
  const count = Number(row.count);
503
696
  if (status in stats) {
@@ -533,6 +726,22 @@ function createWorker(options) {
533
726
  }
534
727
  const result = await query.execute();
535
728
  return Number(result[0]?.numDeletedRows ?? 0);
729
+ },
730
+ wakeUp() {
731
+ if (!running || paused) {
732
+ return;
733
+ }
734
+ if (pumpTimeout) {
735
+ clearTimeout(pumpTimeout);
736
+ pumpTimeout = null;
737
+ }
738
+ pumpTimeout = setTimeout(pump, 0);
739
+ },
740
+ async flushCompletions() {
741
+ await flushDoneBatch();
742
+ },
743
+ attachSignalHandlers(options2) {
744
+ return attachGracefulShutdown(this, options2);
536
745
  }
537
746
  };
538
747
  if (persistState && autoRestore) {
@@ -565,7 +774,15 @@ function createOrchestrator(options) {
565
774
  function ensureEntry(queue) {
566
775
  let entry = registry.get(queue);
567
776
  if (!entry) {
568
- entry = { client: createClient({ db, queue }) };
777
+ entry = {
778
+ client: createClient({
779
+ db,
780
+ queue,
781
+ onJobAdded: () => {
782
+ registry.get(queue)?.worker?.wakeUp();
783
+ }
784
+ })
785
+ };
569
786
  registry.set(queue, entry);
570
787
  }
571
788
  return entry;
@@ -573,7 +790,7 @@ function createOrchestrator(options) {
573
790
  async function setQueuePaused(queue, paused) {
574
791
  const timestamp = now();
575
792
  const value = paused ? 1 : 0;
576
- await sql3`
793
+ await sql2`
577
794
  INSERT INTO workmatic_settings (queue, paused, updated_at)
578
795
  VALUES (${queue}, ${value}, ${timestamp})
579
796
  ON CONFLICT(queue) DO UPDATE SET
@@ -738,11 +955,750 @@ function createOrchestrator(options) {
738
955
  return;
739
956
  }
740
957
  await db.updateTable("workmatic_jobs").set({ queue: toQueue, updated_at: timestamp }).where("public_id", "=", publicId).execute();
958
+ },
959
+ attachSignalHandlers(options2) {
960
+ return attachGracefulShutdown(this, options2);
741
961
  }
742
962
  };
743
963
  return orchestrator;
744
964
  }
745
965
 
966
+ // src/mcp/server.ts
967
+ import readline from "readline";
968
+ import { EventEmitter } from "events";
969
+
970
+ // src/mcp/tools.ts
971
+ import { sql as sql3 } from "kysely";
972
+ var MCP_TOOL_DEFINITIONS = [
973
+ {
974
+ name: "workmatic_list_queues",
975
+ description: "List all queues present in the Workmatic database along with job counts",
976
+ inputSchema: {
977
+ type: "object",
978
+ properties: {}
979
+ }
980
+ },
981
+ {
982
+ name: "workmatic_get_stats",
983
+ description: "Get real-time job counts (ready, running, done, dead, total) for a specific queue or all queues",
984
+ inputSchema: {
985
+ type: "object",
986
+ properties: {
987
+ queue: {
988
+ type: "string",
989
+ description: "Optional queue name. If omitted, stats for all queues will be returned."
990
+ }
991
+ }
992
+ }
993
+ },
994
+ {
995
+ name: "workmatic_list_jobs",
996
+ description: "List jobs in the database filtered by queue, status, and limit",
997
+ inputSchema: {
998
+ type: "object",
999
+ properties: {
1000
+ queue: {
1001
+ type: "string",
1002
+ description: "Filter by queue name"
1003
+ },
1004
+ status: {
1005
+ type: "string",
1006
+ enum: ["ready", "running", "done", "dead"],
1007
+ description: "Filter by job status"
1008
+ },
1009
+ limit: {
1010
+ type: "number",
1011
+ description: "Maximum number of jobs to return (default: 20, max: 100)"
1012
+ },
1013
+ offset: {
1014
+ type: "number",
1015
+ description: "Number of jobs to skip for pagination (default: 0)"
1016
+ }
1017
+ }
1018
+ }
1019
+ },
1020
+ {
1021
+ name: "workmatic_get_dead_jobs",
1022
+ description: "Retrieve failed/dead jobs with error details and payloads for debugging",
1023
+ inputSchema: {
1024
+ type: "object",
1025
+ properties: {
1026
+ queue: {
1027
+ type: "string",
1028
+ description: "Filter dead jobs by queue name"
1029
+ },
1030
+ limit: {
1031
+ type: "number",
1032
+ description: "Maximum number of dead jobs to return (default: 20)"
1033
+ }
1034
+ }
1035
+ }
1036
+ },
1037
+ {
1038
+ name: "workmatic_add_job",
1039
+ description: "Enqueue a new background job into Workmatic",
1040
+ inputSchema: {
1041
+ type: "object",
1042
+ properties: {
1043
+ queue: {
1044
+ type: "string",
1045
+ description: 'Target queue name (default: "default")'
1046
+ },
1047
+ payload: {
1048
+ description: "Job payload (JSON object, string, number, etc.)"
1049
+ },
1050
+ priority: {
1051
+ type: "number",
1052
+ description: "Job priority (lower number = higher priority, default: 0)"
1053
+ },
1054
+ delayMs: {
1055
+ type: "number",
1056
+ description: "Delay in milliseconds before job can run (default: 0)"
1057
+ },
1058
+ maxAttempts: {
1059
+ type: "number",
1060
+ description: "Maximum execution retry attempts (default: 3)"
1061
+ }
1062
+ },
1063
+ required: ["payload"]
1064
+ }
1065
+ },
1066
+ {
1067
+ name: "workmatic_retry_job",
1068
+ description: "Retry a specific dead or failed job by resetting it to ready status",
1069
+ inputSchema: {
1070
+ type: "object",
1071
+ properties: {
1072
+ publicId: {
1073
+ type: "string",
1074
+ description: "Public ID of the job to retry"
1075
+ }
1076
+ },
1077
+ required: ["publicId"]
1078
+ }
1079
+ },
1080
+ {
1081
+ name: "workmatic_retry_all_dead",
1082
+ description: "Retry all dead jobs (optionally in a specific queue) by resetting them to ready status",
1083
+ inputSchema: {
1084
+ type: "object",
1085
+ properties: {
1086
+ queue: {
1087
+ type: "string",
1088
+ description: "Optional queue name to restrict retrying"
1089
+ }
1090
+ }
1091
+ }
1092
+ },
1093
+ {
1094
+ name: "workmatic_pause_queue",
1095
+ description: "Pause a queue so workers stop claiming new jobs from it",
1096
+ inputSchema: {
1097
+ type: "object",
1098
+ properties: {
1099
+ queue: {
1100
+ type: "string",
1101
+ description: "Queue name to pause"
1102
+ }
1103
+ },
1104
+ required: ["queue"]
1105
+ }
1106
+ },
1107
+ {
1108
+ name: "workmatic_resume_queue",
1109
+ description: "Resume a paused queue so workers resume claiming jobs",
1110
+ inputSchema: {
1111
+ type: "object",
1112
+ properties: {
1113
+ queue: {
1114
+ type: "string",
1115
+ description: "Queue name to resume"
1116
+ }
1117
+ },
1118
+ required: ["queue"]
1119
+ }
1120
+ },
1121
+ {
1122
+ name: "workmatic_purge_jobs",
1123
+ description: "Permanently remove done or dead jobs from the database",
1124
+ inputSchema: {
1125
+ type: "object",
1126
+ properties: {
1127
+ queue: {
1128
+ type: "string",
1129
+ description: "Queue name to purge jobs from (optional)"
1130
+ },
1131
+ status: {
1132
+ type: "string",
1133
+ enum: ["done", "dead", "all"],
1134
+ description: 'Status of jobs to purge (default: "done")'
1135
+ }
1136
+ }
1137
+ }
1138
+ },
1139
+ {
1140
+ name: "workmatic_transfer_jobs",
1141
+ description: "Move jobs from one queue to another (e.g. from dead-letter queue back to primary)",
1142
+ inputSchema: {
1143
+ type: "object",
1144
+ properties: {
1145
+ fromQueue: {
1146
+ type: "string",
1147
+ description: "Source queue name"
1148
+ },
1149
+ toQueue: {
1150
+ type: "string",
1151
+ description: "Destination queue name"
1152
+ },
1153
+ status: {
1154
+ type: "string",
1155
+ enum: ["ready", "dead"],
1156
+ description: 'Status of jobs to transfer (default: "ready")'
1157
+ },
1158
+ limit: {
1159
+ type: "number",
1160
+ description: "Maximum number of jobs to transfer (default: 1000)"
1161
+ },
1162
+ resetForRetry: {
1163
+ type: "boolean",
1164
+ description: "If transferring dead jobs, reset their status to ready (default: false)"
1165
+ }
1166
+ },
1167
+ required: ["fromQueue", "toQueue"]
1168
+ }
1169
+ },
1170
+ {
1171
+ name: "workmatic_update_job_status",
1172
+ description: "Update the status of a specific job (ready, done, dead) and signal workers / listeners",
1173
+ inputSchema: {
1174
+ type: "object",
1175
+ properties: {
1176
+ publicId: {
1177
+ type: "string",
1178
+ description: "Public ID of the job to update"
1179
+ },
1180
+ status: {
1181
+ type: "string",
1182
+ enum: ["ready", "done", "dead"],
1183
+ description: "New status for the job"
1184
+ },
1185
+ error: {
1186
+ type: "string",
1187
+ description: "Optional error message when marking as dead or recording failure details"
1188
+ },
1189
+ resetAttempts: {
1190
+ type: "boolean",
1191
+ description: "Whether to reset attempts to 0 (default: true if status is ready, false otherwise)"
1192
+ },
1193
+ delayMs: {
1194
+ type: "number",
1195
+ description: "Delay in milliseconds before the job becomes ready (default: 0)"
1196
+ }
1197
+ },
1198
+ required: ["publicId", "status"]
1199
+ }
1200
+ }
1201
+ ];
1202
+ async function executeTool(db, name, args2 = {}, context) {
1203
+ switch (name) {
1204
+ case "workmatic_list_queues": {
1205
+ const qJobs = await db.selectFrom("workmatic_jobs").select("queue").distinct().execute();
1206
+ const qSettings = await db.selectFrom("workmatic_settings").select("queue").distinct().execute();
1207
+ const set = /* @__PURE__ */ new Set();
1208
+ for (const r of qJobs) set.add(r.queue);
1209
+ for (const r of qSettings) {
1210
+ if (!r.queue.startsWith("worker_state_")) {
1211
+ set.add(r.queue);
1212
+ }
1213
+ }
1214
+ const queueList = Array.from(set).sort();
1215
+ const result = [];
1216
+ for (const q of queueList) {
1217
+ const client = createClient({ db, queue: q });
1218
+ const stats = await client.stats();
1219
+ const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", q).executeTakeFirst();
1220
+ result.push({
1221
+ queue: q,
1222
+ stats: {
1223
+ ready: stats.ready,
1224
+ running: stats.running,
1225
+ done: stats.done,
1226
+ dead: stats.dead,
1227
+ total: stats.total
1228
+ },
1229
+ isPaused: setting?.paused === 1
1230
+ });
1231
+ }
1232
+ return { queues: result, totalQueues: result.length };
1233
+ }
1234
+ case "workmatic_get_stats": {
1235
+ const queue = args2.queue;
1236
+ if (queue) {
1237
+ const client = createClient({ db, queue });
1238
+ return { queue, stats: await client.stats() };
1239
+ }
1240
+ const listRes = await executeTool(db, "workmatic_list_queues");
1241
+ const summary = {};
1242
+ const grandTotal = { ready: 0, running: 0, done: 0, dead: 0, total: 0 };
1243
+ for (const item of listRes.queues) {
1244
+ summary[item.queue] = item.stats;
1245
+ grandTotal.ready += item.stats.ready;
1246
+ grandTotal.running += item.stats.running;
1247
+ grandTotal.done += item.stats.done;
1248
+ grandTotal.dead += item.stats.dead;
1249
+ grandTotal.total += item.stats.total;
1250
+ }
1251
+ return { queues: summary, grandTotal };
1252
+ }
1253
+ case "workmatic_list_jobs": {
1254
+ const queue = args2.queue;
1255
+ const status = args2.status;
1256
+ const limit = Math.min(Math.max(Number(args2.limit ?? 20), 1), 100);
1257
+ const offset = Math.max(Number(args2.offset ?? 0), 0);
1258
+ let query = db.selectFrom("workmatic_jobs").select([
1259
+ "id",
1260
+ "public_id",
1261
+ "queue",
1262
+ "status",
1263
+ "priority",
1264
+ "payload",
1265
+ "attempts",
1266
+ "max_attempts",
1267
+ "run_at",
1268
+ "created_at",
1269
+ "updated_at",
1270
+ "last_error"
1271
+ ]);
1272
+ if (queue) {
1273
+ query = query.where("queue", "=", queue);
1274
+ }
1275
+ if (status) {
1276
+ query = query.where("status", "=", status);
1277
+ }
1278
+ const rows = await query.orderBy("priority", "asc").orderBy("id", "asc").limit(limit).offset(offset).execute();
1279
+ const jobs = rows.map((r) => {
1280
+ let parsedPayload;
1281
+ try {
1282
+ parsedPayload = JSON.parse(r.payload);
1283
+ } catch {
1284
+ parsedPayload = r.payload;
1285
+ }
1286
+ return {
1287
+ id: r.id,
1288
+ publicId: r.public_id,
1289
+ queue: r.queue,
1290
+ status: r.status,
1291
+ priority: r.priority,
1292
+ attempts: r.attempts,
1293
+ maxAttempts: r.max_attempts,
1294
+ runAt: r.run_at,
1295
+ createdAt: r.created_at,
1296
+ updatedAt: r.updated_at,
1297
+ lastError: r.last_error,
1298
+ payload: parsedPayload
1299
+ };
1300
+ });
1301
+ return { jobs, count: jobs.length, limit, offset };
1302
+ }
1303
+ case "workmatic_get_dead_jobs": {
1304
+ const queue = args2.queue;
1305
+ const limit = Math.min(Math.max(Number(args2.limit ?? 20), 1), 100);
1306
+ return executeTool(db, "workmatic_list_jobs", {
1307
+ queue,
1308
+ status: "dead",
1309
+ limit
1310
+ });
1311
+ }
1312
+ case "workmatic_add_job": {
1313
+ const queue = args2.queue || "default";
1314
+ const payload = args2.payload;
1315
+ const priority = args2.priority !== void 0 ? Number(args2.priority) : 0;
1316
+ const delayMs = args2.delayMs !== void 0 ? Number(args2.delayMs) : 0;
1317
+ const maxAttempts = args2.maxAttempts !== void 0 ? Number(args2.maxAttempts) : 3;
1318
+ const client = createClient({ db, queue });
1319
+ const result = await client.add(payload, { priority, delayMs, maxAttempts });
1320
+ return { ok: true, id: result.id, queue };
1321
+ }
1322
+ case "workmatic_retry_job": {
1323
+ const publicId = args2.publicId;
1324
+ if (!publicId) {
1325
+ throw new Error("publicId is required");
1326
+ }
1327
+ const job = await db.selectFrom("workmatic_jobs").select(["queue", "status"]).where("public_id", "=", publicId).executeTakeFirst();
1328
+ if (!job) {
1329
+ throw new Error(`Job not found: ${publicId}`);
1330
+ }
1331
+ const timestamp = now();
1332
+ await db.updateTable("workmatic_jobs").set({
1333
+ status: "ready",
1334
+ attempts: 0,
1335
+ lease_until: 0,
1336
+ last_error: null,
1337
+ run_at: timestamp,
1338
+ updated_at: timestamp
1339
+ }).where("public_id", "=", publicId).execute();
1340
+ context?.onJobStatusChanged?.({
1341
+ publicId,
1342
+ queue: job.queue,
1343
+ previousStatus: job.status,
1344
+ status: "ready",
1345
+ timestamp,
1346
+ error: null
1347
+ });
1348
+ return { ok: true, id: publicId, message: `Job ${publicId} reset to ready` };
1349
+ }
1350
+ case "workmatic_retry_all_dead": {
1351
+ const queue = args2.queue;
1352
+ const timestamp = now();
1353
+ let query = db.updateTable("workmatic_jobs").set({
1354
+ status: "ready",
1355
+ attempts: 0,
1356
+ lease_until: 0,
1357
+ last_error: null,
1358
+ run_at: timestamp,
1359
+ updated_at: timestamp
1360
+ }).where("status", "=", "dead");
1361
+ if (queue) {
1362
+ query = query.where("queue", "=", queue);
1363
+ }
1364
+ const res = await query.execute();
1365
+ const retriedCount = Number(res[0].numUpdatedRows);
1366
+ if (retriedCount > 0) {
1367
+ context?.onJobStatusChanged?.({
1368
+ publicId: "*",
1369
+ queue: queue ?? "*",
1370
+ previousStatus: "dead",
1371
+ status: "ready",
1372
+ timestamp,
1373
+ error: null
1374
+ });
1375
+ }
1376
+ return { ok: true, retriedCount, queue: queue ?? "all" };
1377
+ }
1378
+ case "workmatic_update_job_status": {
1379
+ const publicId = args2.publicId;
1380
+ const status = args2.status;
1381
+ if (!publicId) {
1382
+ throw new Error("publicId is required");
1383
+ }
1384
+ if (!status || !["ready", "done", "dead"].includes(status)) {
1385
+ throw new Error("status is required and must be 'ready', 'done', or 'dead'");
1386
+ }
1387
+ const job = await db.selectFrom("workmatic_jobs").select(["id", "public_id", "queue", "status", "attempts"]).where("public_id", "=", publicId).executeTakeFirst();
1388
+ if (!job) {
1389
+ throw new Error(`Job not found: ${publicId}`);
1390
+ }
1391
+ if (job.status === status) {
1392
+ return {
1393
+ ok: true,
1394
+ id: publicId,
1395
+ queue: job.queue,
1396
+ status,
1397
+ unchanged: true
1398
+ };
1399
+ }
1400
+ const timestamp = now();
1401
+ const previousStatus = job.status;
1402
+ const delayMs = Math.max(Number(args2.delayMs ?? 0), 0);
1403
+ const resetAttempts = args2.resetAttempts !== void 0 ? Boolean(args2.resetAttempts) : status === "ready";
1404
+ const updateData = {
1405
+ status,
1406
+ updated_at: timestamp,
1407
+ lease_until: 0
1408
+ };
1409
+ if (status === "ready") {
1410
+ updateData.run_at = timestamp + delayMs;
1411
+ }
1412
+ if (resetAttempts) {
1413
+ updateData.attempts = 0;
1414
+ }
1415
+ if (args2.error !== void 0) {
1416
+ updateData.last_error = args2.error;
1417
+ } else if (status === "ready") {
1418
+ updateData.last_error = null;
1419
+ }
1420
+ await db.updateTable("workmatic_jobs").set(updateData).where("public_id", "=", publicId).execute();
1421
+ const event = {
1422
+ publicId,
1423
+ queue: job.queue,
1424
+ previousStatus,
1425
+ status,
1426
+ timestamp,
1427
+ error: updateData.last_error ?? null
1428
+ };
1429
+ context?.onJobStatusChanged?.(event);
1430
+ return {
1431
+ ok: true,
1432
+ id: publicId,
1433
+ queue: job.queue,
1434
+ previousStatus,
1435
+ status,
1436
+ signaled: true
1437
+ };
1438
+ }
1439
+ case "workmatic_pause_queue": {
1440
+ const queue = args2.queue;
1441
+ if (!queue) {
1442
+ throw new Error("queue is required");
1443
+ }
1444
+ const timestamp = now();
1445
+ await sql3`
1446
+ INSERT INTO workmatic_settings (queue, paused, updated_at)
1447
+ VALUES (${queue}, 1, ${timestamp})
1448
+ ON CONFLICT(queue) DO UPDATE SET
1449
+ paused = 1,
1450
+ updated_at = ${timestamp}
1451
+ `.execute(db);
1452
+ return { ok: true, queue, paused: true };
1453
+ }
1454
+ case "workmatic_resume_queue": {
1455
+ const queue = args2.queue;
1456
+ if (!queue) {
1457
+ throw new Error("queue is required");
1458
+ }
1459
+ const timestamp = now();
1460
+ await sql3`
1461
+ INSERT INTO workmatic_settings (queue, paused, updated_at)
1462
+ VALUES (${queue}, 0, ${timestamp})
1463
+ ON CONFLICT(queue) DO UPDATE SET
1464
+ paused = 0,
1465
+ updated_at = ${timestamp}
1466
+ `.execute(db);
1467
+ return { ok: true, queue, paused: false };
1468
+ }
1469
+ case "workmatic_purge_jobs": {
1470
+ const queue = args2.queue;
1471
+ const status = args2.status || "done";
1472
+ let query = db.deleteFrom("workmatic_jobs");
1473
+ if (queue) {
1474
+ query = query.where("queue", "=", queue);
1475
+ }
1476
+ if (status !== "all") {
1477
+ query = query.where("status", "=", status);
1478
+ }
1479
+ const res = await query.execute();
1480
+ const deletedCount = Number(res[0].numDeletedRows);
1481
+ return { ok: true, deletedCount, queue: queue ?? "all", status };
1482
+ }
1483
+ case "workmatic_transfer_jobs": {
1484
+ const fromQueue = args2.fromQueue;
1485
+ const toQueue = args2.toQueue;
1486
+ if (!fromQueue || !toQueue) {
1487
+ throw new Error("fromQueue and toQueue are required");
1488
+ }
1489
+ if (fromQueue === toQueue) {
1490
+ return { ok: true, moved: 0 };
1491
+ }
1492
+ const status = args2.status || "ready";
1493
+ const limit = Math.max(Number(args2.limit ?? 1e3), 1);
1494
+ const resetForRetry = Boolean(args2.resetForRetry);
1495
+ const orch = createOrchestrator({ db });
1496
+ const result = await orch.transfer({
1497
+ from: fromQueue,
1498
+ to: toQueue,
1499
+ status,
1500
+ limit,
1501
+ resetForRetry
1502
+ });
1503
+ return {
1504
+ ok: true,
1505
+ moved: result.moved,
1506
+ fromQueue,
1507
+ toQueue,
1508
+ status: resetForRetry && status === "dead" ? "ready" : status
1509
+ };
1510
+ }
1511
+ default:
1512
+ throw new Error(`Unknown tool: ${name}`);
1513
+ }
1514
+ }
1515
+
1516
+ // src/mcp/server.ts
1517
+ function createMcpServer(options) {
1518
+ const db = options.db;
1519
+ const input = options.input ?? process.stdin;
1520
+ const output = options.output ?? process.stdout;
1521
+ const emitter = new EventEmitter();
1522
+ let rl = null;
1523
+ let isRunning = false;
1524
+ function notifyJobStatusChanged(event) {
1525
+ if (options.onJobStatusChanged) {
1526
+ options.onJobStatusChanged(event);
1527
+ }
1528
+ emitter.emit("jobStatusChanged", event);
1529
+ if (event.status === "ready") {
1530
+ if (options.orchestrator) {
1531
+ if (event.queue === "*") {
1532
+ for (const worker of options.orchestrator.workers()) {
1533
+ worker.wakeUp();
1534
+ }
1535
+ } else {
1536
+ try {
1537
+ options.orchestrator.worker(event.queue).wakeUp();
1538
+ } catch {
1539
+ }
1540
+ }
1541
+ }
1542
+ if (options.workers) {
1543
+ for (const worker of options.workers) {
1544
+ if (event.queue === "*" || worker.queue === event.queue) {
1545
+ worker.wakeUp();
1546
+ }
1547
+ }
1548
+ }
1549
+ }
1550
+ if (isRunning) {
1551
+ output.write(
1552
+ JSON.stringify({
1553
+ jsonrpc: "2.0",
1554
+ method: "notifications/workmatic/job_status_changed",
1555
+ params: event
1556
+ }) + "\n"
1557
+ );
1558
+ }
1559
+ }
1560
+ async function handleMessage(raw) {
1561
+ const trimmed = raw.trim();
1562
+ if (!trimmed) {
1563
+ return null;
1564
+ }
1565
+ let msg;
1566
+ try {
1567
+ msg = JSON.parse(trimmed);
1568
+ } catch {
1569
+ return JSON.stringify({
1570
+ jsonrpc: "2.0",
1571
+ id: null,
1572
+ error: {
1573
+ code: -32700,
1574
+ message: "Parse error"
1575
+ }
1576
+ });
1577
+ }
1578
+ if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0" || typeof msg.method !== "string") {
1579
+ return JSON.stringify({
1580
+ jsonrpc: "2.0",
1581
+ id: msg?.id ?? null,
1582
+ error: {
1583
+ code: -32600,
1584
+ message: "Invalid Request"
1585
+ }
1586
+ });
1587
+ }
1588
+ const isNotification = msg.id === void 0;
1589
+ switch (msg.method) {
1590
+ case "initialize": {
1591
+ const result = {
1592
+ protocolVersion: "2024-11-05",
1593
+ capabilities: {
1594
+ tools: {}
1595
+ },
1596
+ serverInfo: {
1597
+ name: "workmatic-mcp",
1598
+ version: "0.1.0"
1599
+ }
1600
+ };
1601
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
1602
+ }
1603
+ case "notifications/initialized": {
1604
+ return null;
1605
+ }
1606
+ case "ping": {
1607
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: {} });
1608
+ }
1609
+ case "tools/list": {
1610
+ const result = {
1611
+ tools: MCP_TOOL_DEFINITIONS
1612
+ };
1613
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
1614
+ }
1615
+ case "tools/call": {
1616
+ if (!msg.params || typeof msg.params.name !== "string") {
1617
+ return isNotification ? null : JSON.stringify({
1618
+ jsonrpc: "2.0",
1619
+ id: msg.id,
1620
+ error: {
1621
+ code: -32602,
1622
+ message: 'Invalid params: tool "name" is required'
1623
+ }
1624
+ });
1625
+ }
1626
+ const toolName = msg.params.name;
1627
+ const toolArgs = msg.params.arguments ?? {};
1628
+ try {
1629
+ const toolResult = await executeTool(db, toolName, toolArgs, {
1630
+ onJobStatusChanged: notifyJobStatusChanged
1631
+ });
1632
+ const response = {
1633
+ content: [
1634
+ {
1635
+ type: "text",
1636
+ text: JSON.stringify(toolResult, null, 2)
1637
+ }
1638
+ ]
1639
+ };
1640
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: response });
1641
+ } catch (err) {
1642
+ const errorMessage = err instanceof Error ? err.message : String(err);
1643
+ const response = {
1644
+ content: [
1645
+ {
1646
+ type: "text",
1647
+ text: errorMessage
1648
+ }
1649
+ ],
1650
+ isError: true
1651
+ };
1652
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: response });
1653
+ }
1654
+ }
1655
+ default: {
1656
+ return isNotification ? null : JSON.stringify({
1657
+ jsonrpc: "2.0",
1658
+ id: msg.id,
1659
+ error: {
1660
+ code: -32601,
1661
+ message: `Method not found: ${msg.method}`
1662
+ }
1663
+ });
1664
+ }
1665
+ }
1666
+ }
1667
+ const server = {
1668
+ start() {
1669
+ if (isRunning) return;
1670
+ isRunning = true;
1671
+ rl = readline.createInterface({
1672
+ input,
1673
+ terminal: false
1674
+ });
1675
+ rl.on("line", (line) => {
1676
+ void handleMessage(line).then((response) => {
1677
+ if (response && isRunning) {
1678
+ output.write(response + "\n");
1679
+ }
1680
+ });
1681
+ });
1682
+ },
1683
+ stop() {
1684
+ if (!isRunning) return;
1685
+ isRunning = false;
1686
+ rl.close();
1687
+ rl = null;
1688
+ },
1689
+ handleMessage,
1690
+ on(event, listener) {
1691
+ emitter.on(event, listener);
1692
+ return server;
1693
+ },
1694
+ off(event, listener) {
1695
+ emitter.off(event, listener);
1696
+ return server;
1697
+ }
1698
+ };
1699
+ return server;
1700
+ }
1701
+
746
1702
  // src/cli/handlers.ts
747
1703
  function printUsage() {
748
1704
  console.log(`
@@ -762,6 +1718,8 @@ Commands:
762
1718
  pause <db> <queue> Pause a queue (running workers stop claiming)
763
1719
  resume <db> <queue> Resume a paused queue
764
1720
  transfer <db> <from> <to> Move jobs between queues
1721
+ mcp <db> Start Model Context Protocol (MCP) server
1722
+
765
1723
 
766
1724
  Options:
767
1725
  --status=<status> Filter by status (ready|running|done|dead), comma-separated for transfer
@@ -1235,6 +2193,34 @@ async function cmdTransfer(dbPath, from, to, options) {
1235
2193
  await db.destroy();
1236
2194
  }
1237
2195
  }
2196
+ async function cmdMcp(dbPath, options = {}) {
2197
+ const db = createDatabase({ filename: dbPath });
2198
+ const server = createMcpServer({
2199
+ db,
2200
+ input: options.input,
2201
+ output: options.output
2202
+ });
2203
+ server.start();
2204
+ await new Promise((resolve) => {
2205
+ const cleanup = () => {
2206
+ server.stop();
2207
+ if (!options.input) {
2208
+ process.removeListener("SIGINT", cleanup);
2209
+ process.removeListener("SIGTERM", cleanup);
2210
+ process.stdin.removeListener("end", cleanup);
2211
+ }
2212
+ resolve();
2213
+ };
2214
+ if (options.input) {
2215
+ options.input.once("end", cleanup);
2216
+ } else {
2217
+ process.stdin.once("end", cleanup);
2218
+ process.once("SIGINT", cleanup);
2219
+ process.once("SIGTERM", cleanup);
2220
+ }
2221
+ });
2222
+ await db.destroy();
2223
+ }
1238
2224
  async function runCommand(command2, dbPath, positionalArgs, options) {
1239
2225
  switch (command2) {
1240
2226
  case "stats":
@@ -1279,6 +2265,9 @@ async function runCommand(command2, dbPath, positionalArgs, options) {
1279
2265
  }
1280
2266
  await cmdTransfer(dbPath, positionalArgs[1], positionalArgs[2], options);
1281
2267
  break;
2268
+ case "mcp":
2269
+ await cmdMcp(dbPath);
2270
+ break;
1282
2271
  default:
1283
2272
  throw new Error(`Unknown command: ${command2}`);
1284
2273
  }
@@ -1297,7 +2286,8 @@ var KNOWN_COMMANDS = [
1297
2286
  "pause",
1298
2287
  "resume",
1299
2288
  "queues",
1300
- "transfer"
2289
+ "transfer",
2290
+ "mcp"
1301
2291
  ];
1302
2292
  function isCliCommand(value) {
1303
2293
  return KNOWN_COMMANDS.includes(value);