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.cjs CHANGED
@@ -45,6 +45,11 @@ function createDatabase(options = {}) {
45
45
  sqliteDb.pragma("journal_mode = WAL");
46
46
  sqliteDb.pragma("synchronous = NORMAL");
47
47
  sqliteDb.pragma("busy_timeout = 5000");
48
+ sqliteDb.pragma("cache_size = -64000");
49
+ sqliteDb.pragma("temp_store = MEMORY");
50
+ sqliteDb.pragma("mmap_size = 268435456");
51
+ const cacheSize = options.statementCacheSize ?? 1e3;
52
+ enableStatementCache(sqliteDb, cacheSize);
48
53
  const db = new import_kysely.Kysely({
49
54
  dialect: new import_kysely.SqliteDialect({
50
55
  database: sqliteDb
@@ -72,13 +77,23 @@ function createSchema(db) {
72
77
  last_error TEXT
73
78
  )
74
79
  `);
80
+ ensurePartialIndex(
81
+ db,
82
+ "idx_workmatic_jobs_claim",
83
+ `CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_claim
84
+ ON workmatic_jobs (queue, status, run_at, priority, id)
85
+ WHERE status = 'ready'`
86
+ );
87
+ ensurePartialIndex(
88
+ db,
89
+ "idx_workmatic_jobs_lease",
90
+ `CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_lease
91
+ ON workmatic_jobs (status, lease_until)
92
+ WHERE status = 'running'`
93
+ );
75
94
  db.exec(`
76
- CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_claim
77
- ON workmatic_jobs (queue, status, run_at, priority, id)
78
- `);
79
- db.exec(`
80
- CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_lease
81
- ON workmatic_jobs (status, lease_until)
95
+ CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_queue_status
96
+ ON workmatic_jobs (queue, status)
82
97
  `);
83
98
  db.exec(`
84
99
  CREATE TABLE IF NOT EXISTS workmatic_settings (
@@ -91,6 +106,46 @@ function createSchema(db) {
91
106
  UPDATE workmatic_jobs SET status = 'dead' WHERE status = 'failed'
92
107
  `);
93
108
  }
109
+ function ensurePartialIndex(db, indexName, createSql) {
110
+ const row = db.prepare(
111
+ "SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?"
112
+ ).get(indexName);
113
+ if (row?.sql && !row.sql.toUpperCase().includes("WHERE")) {
114
+ db.exec(`DROP INDEX IF EXISTS ${indexName}`);
115
+ }
116
+ db.exec(createSql);
117
+ }
118
+ function enableStatementCache(db, maxStatements = 1e3) {
119
+ if (maxStatements <= 0) {
120
+ return db;
121
+ }
122
+ const originalPrepare = db.prepare.bind(db);
123
+ const cache = /* @__PURE__ */ new Map();
124
+ db.prepare = function(sql4) {
125
+ const cached = cache.get(sql4);
126
+ if (cached) {
127
+ cache.delete(sql4);
128
+ cache.set(sql4, cached);
129
+ if (!cached.busy) {
130
+ return cached;
131
+ }
132
+ return originalPrepare(sql4);
133
+ }
134
+ const stmt = originalPrepare(sql4);
135
+ if (cache.size >= maxStatements) {
136
+ const oldestKey = cache.keys().next().value;
137
+ cache.delete(oldestKey);
138
+ }
139
+ cache.set(sql4, stmt);
140
+ return stmt;
141
+ };
142
+ const originalClose = db.close.bind(db);
143
+ db.close = function() {
144
+ cache.clear();
145
+ return originalClose();
146
+ };
147
+ return db;
148
+ }
94
149
 
95
150
  // src/orchestrator.ts
96
151
  var import_kysely4 = require("kysely");
@@ -127,10 +182,16 @@ function now() {
127
182
 
128
183
  // src/client.ts
129
184
  function createClient(options) {
130
- const { db, queue = "default" } = options;
185
+ const { db, queue = "default", onJobAdded, worker } = options;
131
186
  if (!db) {
132
187
  throw new Error("Database instance is required");
133
188
  }
189
+ function notifyJobAdded(delayMs) {
190
+ if (delayMs <= 0) {
191
+ worker?.wakeUp();
192
+ onJobAdded?.();
193
+ }
194
+ }
134
195
  return {
135
196
  /**
136
197
  * Add a job to the queue
@@ -145,20 +206,13 @@ function createClient(options) {
145
206
  const publicId = (0, import_nanoid.nanoid)();
146
207
  const timestamp = now();
147
208
  const runAt = timestamp + delayMs;
148
- await db.insertInto("workmatic_jobs").values({
149
- public_id: publicId,
150
- queue,
151
- payload: payloadJson,
152
- status: "ready",
153
- priority,
154
- run_at: runAt,
155
- attempts: 0,
156
- max_attempts: maxAttempts,
157
- lease_until: 0,
158
- created_at: timestamp,
159
- updated_at: timestamp,
160
- last_error: null
161
- }).execute();
209
+ await db.executeQuery(
210
+ import_kysely2.CompiledQuery.raw(
211
+ `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)`,
212
+ [publicId, queue, payloadJson, priority, runAt, maxAttempts, timestamp, timestamp]
213
+ )
214
+ );
215
+ notifyJobAdded(delayMs);
162
216
  return { ok: true, id: publicId };
163
217
  },
164
218
  async addMany(payloads, opts = {}) {
@@ -172,7 +226,7 @@ function createClient(options) {
172
226
  }
173
227
  const timestamp = now();
174
228
  const runAt = timestamp + delayMs;
175
- return await db.transaction().execute(async (trx) => {
229
+ const result = await db.transaction().execute(async (trx) => {
176
230
  const ids = [];
177
231
  const rows = payloads.map((payload) => {
178
232
  const payloadJson = validatePayload(payload);
@@ -196,15 +250,19 @@ function createClient(options) {
196
250
  await trx.insertInto("workmatic_jobs").values(rows).execute();
197
251
  return { ok: true, ids };
198
252
  });
253
+ notifyJobAdded(delayMs);
254
+ return result;
199
255
  },
200
256
  /**
201
257
  * Get job statistics for the queue
202
258
  */
203
259
  async stats() {
204
- const result = await db.selectFrom("workmatic_jobs").select([
205
- "status",
206
- import_kysely2.sql`count(*)`.as("count")
207
- ]).where("queue", "=", queue).groupBy("status").execute();
260
+ const result = await db.executeQuery(
261
+ import_kysely2.CompiledQuery.raw(
262
+ "SELECT status, count(*) AS count FROM workmatic_jobs WHERE queue = ? GROUP BY status",
263
+ [queue]
264
+ )
265
+ );
208
266
  const stats = {
209
267
  ready: 0,
210
268
  running: 0,
@@ -212,7 +270,7 @@ function createClient(options) {
212
270
  dead: 0,
213
271
  total: 0
214
272
  };
215
- for (const row of result) {
273
+ for (const row of result.rows) {
216
274
  const status = row.status;
217
275
  const count = Number(row.count);
218
276
  if (status in stats) {
@@ -239,6 +297,76 @@ function createClient(options) {
239
297
  // src/worker.ts
240
298
  var import_fastq = __toESM(require("fastq"), 1);
241
299
  var import_kysely3 = require("kysely");
300
+
301
+ // src/shutdown.ts
302
+ function attachGracefulShutdown(target, options = {}) {
303
+ const {
304
+ signals = ["SIGINT", "SIGTERM"],
305
+ timeoutMs = 3e4,
306
+ exitOnComplete = true,
307
+ exitCode = 0,
308
+ timeoutExitCode = 1,
309
+ onShutdownStart,
310
+ onShutdownComplete,
311
+ onShutdownError
312
+ } = options;
313
+ let shuttingDown = false;
314
+ async function stopTarget() {
315
+ if (Array.isArray(target)) {
316
+ await Promise.all(target.map((w) => w.stop()));
317
+ } else if ("stopAll" in target) {
318
+ await target.stopAll();
319
+ } else {
320
+ await target.stop();
321
+ }
322
+ }
323
+ const handler = async (signal) => {
324
+ if (shuttingDown) {
325
+ return;
326
+ }
327
+ shuttingDown = true;
328
+ onShutdownStart?.(signal);
329
+ let timer = null;
330
+ if (timeoutMs > 0) {
331
+ timer = setTimeout(() => {
332
+ const err = new Error(`Graceful shutdown timed out after ${timeoutMs}ms`);
333
+ onShutdownError?.(err);
334
+ if (exitOnComplete) {
335
+ process.exit(timeoutExitCode);
336
+ }
337
+ }, timeoutMs);
338
+ timer.unref();
339
+ }
340
+ try {
341
+ await stopTarget();
342
+ if (timer) {
343
+ clearTimeout(timer);
344
+ }
345
+ onShutdownComplete?.();
346
+ if (exitOnComplete) {
347
+ process.exit(exitCode);
348
+ }
349
+ } catch (err) {
350
+ if (timer) {
351
+ clearTimeout(timer);
352
+ }
353
+ onShutdownError?.(err);
354
+ if (exitOnComplete) {
355
+ process.exit(timeoutExitCode);
356
+ }
357
+ }
358
+ };
359
+ for (const sig of signals) {
360
+ process.on(sig, handler);
361
+ }
362
+ return function detach() {
363
+ for (const sig of signals) {
364
+ process.removeListener(sig, handler);
365
+ }
366
+ };
367
+ }
368
+
369
+ // src/worker.ts
242
370
  var DEFAULT_WORKER_TIMEOUT_MS = 6e4;
243
371
  function parseClaimedRows(result) {
244
372
  const rows = result.rows;
@@ -276,7 +404,8 @@ function createWorker(options) {
276
404
  autoRestore = true,
277
405
  pauseCheckIntervalMs = 300,
278
406
  requeueExpiredIntervalMs = 0,
279
- onPumpError
407
+ onPumpError,
408
+ completionBatchSize = 50
280
409
  } = options;
281
410
  if (!db) {
282
411
  throw new Error("Database instance is required");
@@ -289,6 +418,8 @@ function createWorker(options) {
289
418
  let lastPauseCheckAt = 0;
290
419
  let cachedDbPaused = false;
291
420
  let lastRequeueAt = 0;
421
+ let pendingDone = [];
422
+ let flushTimeout = null;
292
423
  function notifyPumpError(error) {
293
424
  console.error("[workmatic] Pump error:", error);
294
425
  onPumpError?.(error);
@@ -336,52 +467,104 @@ function createWorker(options) {
336
467
  async function claimBatch(limit) {
337
468
  const timestamp = now();
338
469
  const leaseUntil = timestamp + leaseMs;
339
- return await db.transaction().execute(async (trx) => {
340
- const result = await import_kysely3.sql`
341
- UPDATE workmatic_jobs
342
- SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
343
- WHERE rowid IN (
344
- SELECT rowid FROM workmatic_jobs
345
- WHERE queue = ${queue}
346
- AND status = 'ready'
347
- AND run_at <= ${timestamp}
348
- ORDER BY priority ASC, id ASC
349
- LIMIT ${limit}
470
+ const result = await import_kysely3.sql`
471
+ UPDATE workmatic_jobs
472
+ SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
473
+ WHERE rowid IN (
474
+ SELECT rowid FROM workmatic_jobs
475
+ WHERE queue = ${queue}
476
+ AND status = 'ready'
477
+ AND run_at <= ${timestamp}
478
+ ORDER BY priority ASC, id ASC
479
+ LIMIT ${limit}
480
+ )
481
+ RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
482
+ `.execute(db);
483
+ return parseClaimedRows(result);
484
+ }
485
+ async function flushDoneBatch() {
486
+ if (flushTimeout !== null) {
487
+ clearImmediate(flushTimeout);
488
+ flushTimeout = null;
489
+ }
490
+ if (pendingDone.length === 0) {
491
+ return;
492
+ }
493
+ const current = pendingDone;
494
+ pendingDone = [];
495
+ const timestamp = now();
496
+ try {
497
+ if (current.length === 1) {
498
+ await db.executeQuery(
499
+ import_kysely3.CompiledQuery.raw(
500
+ "UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id = ?",
501
+ [timestamp, current[0].id]
502
+ )
503
+ );
504
+ } else {
505
+ const CHUNK_SIZE = 500;
506
+ for (let i = 0; i < current.length; i += CHUNK_SIZE) {
507
+ const chunk = current.slice(i, i + CHUNK_SIZE);
508
+ const placeholders = chunk.map(() => "?").join(", ");
509
+ const params = [timestamp, ...chunk.map((item) => item.id)];
510
+ await db.executeQuery(
511
+ import_kysely3.CompiledQuery.raw(
512
+ `UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id IN (${placeholders})`,
513
+ params
514
+ )
515
+ );
516
+ }
517
+ }
518
+ for (const item of current) {
519
+ item.resolve();
520
+ }
521
+ } catch (err) {
522
+ for (const item of current) {
523
+ item.reject(err);
524
+ }
525
+ }
526
+ }
527
+ function markDone(jobId) {
528
+ if (completionBatchSize <= 0) {
529
+ return db.executeQuery(
530
+ import_kysely3.CompiledQuery.raw(
531
+ "UPDATE workmatic_jobs SET status = 'done', lease_until = 0, updated_at = ? WHERE id = ?",
532
+ [now(), jobId]
350
533
  )
351
- RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
352
- `.execute(trx);
353
- return parseClaimedRows(result);
534
+ ).then(() => {
535
+ });
536
+ }
537
+ return new Promise((resolve, reject) => {
538
+ pendingDone.push({ id: jobId, resolve, reject });
539
+ if (pendingDone.length >= completionBatchSize) {
540
+ void flushDoneBatch();
541
+ } else if (!flushTimeout) {
542
+ flushTimeout = setImmediate(() => {
543
+ flushTimeout = null;
544
+ void flushDoneBatch();
545
+ });
546
+ }
354
547
  });
355
548
  }
356
- async function markDone(jobId) {
357
- await db.updateTable("workmatic_jobs").set({
358
- status: "done",
359
- lease_until: 0,
360
- updated_at: now()
361
- }).where("id", "=", jobId).execute();
362
- }
363
549
  async function markFailed(jobId, attempts, maxAttempts, error) {
364
550
  const timestamp = now();
365
551
  const newAttempts = attempts + 1;
366
552
  const errorMessage = error.message || String(error);
367
553
  if (newAttempts < maxAttempts) {
368
554
  const runAt = timestamp + backoff(newAttempts);
369
- await db.updateTable("workmatic_jobs").set({
370
- status: "ready",
371
- attempts: newAttempts,
372
- run_at: runAt,
373
- lease_until: 0,
374
- last_error: errorMessage,
375
- updated_at: timestamp
376
- }).where("id", "=", jobId).execute();
555
+ await db.executeQuery(
556
+ import_kysely3.CompiledQuery.raw(
557
+ "UPDATE workmatic_jobs SET status = 'ready', attempts = ?, run_at = ?, lease_until = 0, last_error = ?, updated_at = ? WHERE id = ?",
558
+ [newAttempts, runAt, errorMessage, timestamp, jobId]
559
+ )
560
+ );
377
561
  } else {
378
- await db.updateTable("workmatic_jobs").set({
379
- status: "dead",
380
- attempts: newAttempts,
381
- lease_until: 0,
382
- last_error: errorMessage,
383
- updated_at: timestamp
384
- }).where("id", "=", jobId).execute();
562
+ await db.executeQuery(
563
+ import_kysely3.CompiledQuery.raw(
564
+ "UPDATE workmatic_jobs SET status = 'dead', attempts = ?, lease_until = 0, last_error = ?, updated_at = ? WHERE id = ?",
565
+ [newAttempts, errorMessage, timestamp, jobId]
566
+ )
567
+ );
385
568
  }
386
569
  }
387
570
  async function withTimeout(promise, ms, jobId) {
@@ -428,8 +611,13 @@ function createWorker(options) {
428
611
  }
429
612
  }
430
613
  async function isQueuePausedInDb() {
431
- const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", queue).executeTakeFirst();
432
- return setting?.paused === 1;
614
+ const result = await db.executeQuery(
615
+ import_kysely3.CompiledQuery.raw(
616
+ "SELECT paused FROM workmatic_settings WHERE queue = ? LIMIT 1",
617
+ [queue]
618
+ )
619
+ );
620
+ return result.rows[0]?.paused === 1;
433
621
  }
434
622
  async function pump() {
435
623
  if (!running) {
@@ -498,10 +686,12 @@ function createWorker(options) {
498
686
  }
499
687
  await fastqQueue.drained();
500
688
  fastqQueue = null;
689
+ await flushDoneBatch();
501
690
  await saveState("stopped");
502
691
  },
503
692
  pause() {
504
693
  paused = true;
694
+ void flushDoneBatch();
505
695
  void saveState("paused");
506
696
  },
507
697
  resume() {
@@ -509,10 +699,13 @@ function createWorker(options) {
509
699
  void saveState("running");
510
700
  },
511
701
  async stats() {
512
- const result = await db.selectFrom("workmatic_jobs").select([
513
- "status",
514
- import_kysely3.sql`count(*)`.as("count")
515
- ]).where("queue", "=", queue).groupBy("status").execute();
702
+ await flushDoneBatch();
703
+ const result = await db.executeQuery(
704
+ import_kysely3.CompiledQuery.raw(
705
+ "SELECT status, count(*) AS count FROM workmatic_jobs WHERE queue = ? GROUP BY status",
706
+ [queue]
707
+ )
708
+ );
516
709
  const stats = {
517
710
  ready: 0,
518
711
  running: 0,
@@ -520,7 +713,7 @@ function createWorker(options) {
520
713
  dead: 0,
521
714
  total: 0
522
715
  };
523
- for (const row of result) {
716
+ for (const row of result.rows) {
524
717
  const status = row.status;
525
718
  const count = Number(row.count);
526
719
  if (status in stats) {
@@ -556,6 +749,22 @@ function createWorker(options) {
556
749
  }
557
750
  const result = await query.execute();
558
751
  return Number(result[0]?.numDeletedRows ?? 0);
752
+ },
753
+ wakeUp() {
754
+ if (!running || paused) {
755
+ return;
756
+ }
757
+ if (pumpTimeout) {
758
+ clearTimeout(pumpTimeout);
759
+ pumpTimeout = null;
760
+ }
761
+ pumpTimeout = setTimeout(pump, 0);
762
+ },
763
+ async flushCompletions() {
764
+ await flushDoneBatch();
765
+ },
766
+ attachSignalHandlers(options2) {
767
+ return attachGracefulShutdown(this, options2);
559
768
  }
560
769
  };
561
770
  if (persistState && autoRestore) {
@@ -588,7 +797,15 @@ function createOrchestrator(options) {
588
797
  function ensureEntry(queue) {
589
798
  let entry = registry.get(queue);
590
799
  if (!entry) {
591
- entry = { client: createClient({ db, queue }) };
800
+ entry = {
801
+ client: createClient({
802
+ db,
803
+ queue,
804
+ onJobAdded: () => {
805
+ registry.get(queue)?.worker?.wakeUp();
806
+ }
807
+ })
808
+ };
592
809
  registry.set(queue, entry);
593
810
  }
594
811
  return entry;
@@ -761,11 +978,750 @@ function createOrchestrator(options) {
761
978
  return;
762
979
  }
763
980
  await db.updateTable("workmatic_jobs").set({ queue: toQueue, updated_at: timestamp }).where("public_id", "=", publicId).execute();
981
+ },
982
+ attachSignalHandlers(options2) {
983
+ return attachGracefulShutdown(this, options2);
764
984
  }
765
985
  };
766
986
  return orchestrator;
767
987
  }
768
988
 
989
+ // src/mcp/server.ts
990
+ var import_node_readline = __toESM(require("readline"), 1);
991
+ var import_node_events = require("events");
992
+
993
+ // src/mcp/tools.ts
994
+ var import_kysely5 = require("kysely");
995
+ var MCP_TOOL_DEFINITIONS = [
996
+ {
997
+ name: "workmatic_list_queues",
998
+ description: "List all queues present in the Workmatic database along with job counts",
999
+ inputSchema: {
1000
+ type: "object",
1001
+ properties: {}
1002
+ }
1003
+ },
1004
+ {
1005
+ name: "workmatic_get_stats",
1006
+ description: "Get real-time job counts (ready, running, done, dead, total) for a specific queue or all queues",
1007
+ inputSchema: {
1008
+ type: "object",
1009
+ properties: {
1010
+ queue: {
1011
+ type: "string",
1012
+ description: "Optional queue name. If omitted, stats for all queues will be returned."
1013
+ }
1014
+ }
1015
+ }
1016
+ },
1017
+ {
1018
+ name: "workmatic_list_jobs",
1019
+ description: "List jobs in the database filtered by queue, status, and limit",
1020
+ inputSchema: {
1021
+ type: "object",
1022
+ properties: {
1023
+ queue: {
1024
+ type: "string",
1025
+ description: "Filter by queue name"
1026
+ },
1027
+ status: {
1028
+ type: "string",
1029
+ enum: ["ready", "running", "done", "dead"],
1030
+ description: "Filter by job status"
1031
+ },
1032
+ limit: {
1033
+ type: "number",
1034
+ description: "Maximum number of jobs to return (default: 20, max: 100)"
1035
+ },
1036
+ offset: {
1037
+ type: "number",
1038
+ description: "Number of jobs to skip for pagination (default: 0)"
1039
+ }
1040
+ }
1041
+ }
1042
+ },
1043
+ {
1044
+ name: "workmatic_get_dead_jobs",
1045
+ description: "Retrieve failed/dead jobs with error details and payloads for debugging",
1046
+ inputSchema: {
1047
+ type: "object",
1048
+ properties: {
1049
+ queue: {
1050
+ type: "string",
1051
+ description: "Filter dead jobs by queue name"
1052
+ },
1053
+ limit: {
1054
+ type: "number",
1055
+ description: "Maximum number of dead jobs to return (default: 20)"
1056
+ }
1057
+ }
1058
+ }
1059
+ },
1060
+ {
1061
+ name: "workmatic_add_job",
1062
+ description: "Enqueue a new background job into Workmatic",
1063
+ inputSchema: {
1064
+ type: "object",
1065
+ properties: {
1066
+ queue: {
1067
+ type: "string",
1068
+ description: 'Target queue name (default: "default")'
1069
+ },
1070
+ payload: {
1071
+ description: "Job payload (JSON object, string, number, etc.)"
1072
+ },
1073
+ priority: {
1074
+ type: "number",
1075
+ description: "Job priority (lower number = higher priority, default: 0)"
1076
+ },
1077
+ delayMs: {
1078
+ type: "number",
1079
+ description: "Delay in milliseconds before job can run (default: 0)"
1080
+ },
1081
+ maxAttempts: {
1082
+ type: "number",
1083
+ description: "Maximum execution retry attempts (default: 3)"
1084
+ }
1085
+ },
1086
+ required: ["payload"]
1087
+ }
1088
+ },
1089
+ {
1090
+ name: "workmatic_retry_job",
1091
+ description: "Retry a specific dead or failed job by resetting it to ready status",
1092
+ inputSchema: {
1093
+ type: "object",
1094
+ properties: {
1095
+ publicId: {
1096
+ type: "string",
1097
+ description: "Public ID of the job to retry"
1098
+ }
1099
+ },
1100
+ required: ["publicId"]
1101
+ }
1102
+ },
1103
+ {
1104
+ name: "workmatic_retry_all_dead",
1105
+ description: "Retry all dead jobs (optionally in a specific queue) by resetting them to ready status",
1106
+ inputSchema: {
1107
+ type: "object",
1108
+ properties: {
1109
+ queue: {
1110
+ type: "string",
1111
+ description: "Optional queue name to restrict retrying"
1112
+ }
1113
+ }
1114
+ }
1115
+ },
1116
+ {
1117
+ name: "workmatic_pause_queue",
1118
+ description: "Pause a queue so workers stop claiming new jobs from it",
1119
+ inputSchema: {
1120
+ type: "object",
1121
+ properties: {
1122
+ queue: {
1123
+ type: "string",
1124
+ description: "Queue name to pause"
1125
+ }
1126
+ },
1127
+ required: ["queue"]
1128
+ }
1129
+ },
1130
+ {
1131
+ name: "workmatic_resume_queue",
1132
+ description: "Resume a paused queue so workers resume claiming jobs",
1133
+ inputSchema: {
1134
+ type: "object",
1135
+ properties: {
1136
+ queue: {
1137
+ type: "string",
1138
+ description: "Queue name to resume"
1139
+ }
1140
+ },
1141
+ required: ["queue"]
1142
+ }
1143
+ },
1144
+ {
1145
+ name: "workmatic_purge_jobs",
1146
+ description: "Permanently remove done or dead jobs from the database",
1147
+ inputSchema: {
1148
+ type: "object",
1149
+ properties: {
1150
+ queue: {
1151
+ type: "string",
1152
+ description: "Queue name to purge jobs from (optional)"
1153
+ },
1154
+ status: {
1155
+ type: "string",
1156
+ enum: ["done", "dead", "all"],
1157
+ description: 'Status of jobs to purge (default: "done")'
1158
+ }
1159
+ }
1160
+ }
1161
+ },
1162
+ {
1163
+ name: "workmatic_transfer_jobs",
1164
+ description: "Move jobs from one queue to another (e.g. from dead-letter queue back to primary)",
1165
+ inputSchema: {
1166
+ type: "object",
1167
+ properties: {
1168
+ fromQueue: {
1169
+ type: "string",
1170
+ description: "Source queue name"
1171
+ },
1172
+ toQueue: {
1173
+ type: "string",
1174
+ description: "Destination queue name"
1175
+ },
1176
+ status: {
1177
+ type: "string",
1178
+ enum: ["ready", "dead"],
1179
+ description: 'Status of jobs to transfer (default: "ready")'
1180
+ },
1181
+ limit: {
1182
+ type: "number",
1183
+ description: "Maximum number of jobs to transfer (default: 1000)"
1184
+ },
1185
+ resetForRetry: {
1186
+ type: "boolean",
1187
+ description: "If transferring dead jobs, reset their status to ready (default: false)"
1188
+ }
1189
+ },
1190
+ required: ["fromQueue", "toQueue"]
1191
+ }
1192
+ },
1193
+ {
1194
+ name: "workmatic_update_job_status",
1195
+ description: "Update the status of a specific job (ready, done, dead) and signal workers / listeners",
1196
+ inputSchema: {
1197
+ type: "object",
1198
+ properties: {
1199
+ publicId: {
1200
+ type: "string",
1201
+ description: "Public ID of the job to update"
1202
+ },
1203
+ status: {
1204
+ type: "string",
1205
+ enum: ["ready", "done", "dead"],
1206
+ description: "New status for the job"
1207
+ },
1208
+ error: {
1209
+ type: "string",
1210
+ description: "Optional error message when marking as dead or recording failure details"
1211
+ },
1212
+ resetAttempts: {
1213
+ type: "boolean",
1214
+ description: "Whether to reset attempts to 0 (default: true if status is ready, false otherwise)"
1215
+ },
1216
+ delayMs: {
1217
+ type: "number",
1218
+ description: "Delay in milliseconds before the job becomes ready (default: 0)"
1219
+ }
1220
+ },
1221
+ required: ["publicId", "status"]
1222
+ }
1223
+ }
1224
+ ];
1225
+ async function executeTool(db, name, args2 = {}, context) {
1226
+ switch (name) {
1227
+ case "workmatic_list_queues": {
1228
+ const qJobs = await db.selectFrom("workmatic_jobs").select("queue").distinct().execute();
1229
+ const qSettings = await db.selectFrom("workmatic_settings").select("queue").distinct().execute();
1230
+ const set = /* @__PURE__ */ new Set();
1231
+ for (const r of qJobs) set.add(r.queue);
1232
+ for (const r of qSettings) {
1233
+ if (!r.queue.startsWith("worker_state_")) {
1234
+ set.add(r.queue);
1235
+ }
1236
+ }
1237
+ const queueList = Array.from(set).sort();
1238
+ const result = [];
1239
+ for (const q of queueList) {
1240
+ const client = createClient({ db, queue: q });
1241
+ const stats = await client.stats();
1242
+ const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", q).executeTakeFirst();
1243
+ result.push({
1244
+ queue: q,
1245
+ stats: {
1246
+ ready: stats.ready,
1247
+ running: stats.running,
1248
+ done: stats.done,
1249
+ dead: stats.dead,
1250
+ total: stats.total
1251
+ },
1252
+ isPaused: setting?.paused === 1
1253
+ });
1254
+ }
1255
+ return { queues: result, totalQueues: result.length };
1256
+ }
1257
+ case "workmatic_get_stats": {
1258
+ const queue = args2.queue;
1259
+ if (queue) {
1260
+ const client = createClient({ db, queue });
1261
+ return { queue, stats: await client.stats() };
1262
+ }
1263
+ const listRes = await executeTool(db, "workmatic_list_queues");
1264
+ const summary = {};
1265
+ const grandTotal = { ready: 0, running: 0, done: 0, dead: 0, total: 0 };
1266
+ for (const item of listRes.queues) {
1267
+ summary[item.queue] = item.stats;
1268
+ grandTotal.ready += item.stats.ready;
1269
+ grandTotal.running += item.stats.running;
1270
+ grandTotal.done += item.stats.done;
1271
+ grandTotal.dead += item.stats.dead;
1272
+ grandTotal.total += item.stats.total;
1273
+ }
1274
+ return { queues: summary, grandTotal };
1275
+ }
1276
+ case "workmatic_list_jobs": {
1277
+ const queue = args2.queue;
1278
+ const status = args2.status;
1279
+ const limit = Math.min(Math.max(Number(args2.limit ?? 20), 1), 100);
1280
+ const offset = Math.max(Number(args2.offset ?? 0), 0);
1281
+ let query = db.selectFrom("workmatic_jobs").select([
1282
+ "id",
1283
+ "public_id",
1284
+ "queue",
1285
+ "status",
1286
+ "priority",
1287
+ "payload",
1288
+ "attempts",
1289
+ "max_attempts",
1290
+ "run_at",
1291
+ "created_at",
1292
+ "updated_at",
1293
+ "last_error"
1294
+ ]);
1295
+ if (queue) {
1296
+ query = query.where("queue", "=", queue);
1297
+ }
1298
+ if (status) {
1299
+ query = query.where("status", "=", status);
1300
+ }
1301
+ const rows = await query.orderBy("priority", "asc").orderBy("id", "asc").limit(limit).offset(offset).execute();
1302
+ const jobs = rows.map((r) => {
1303
+ let parsedPayload;
1304
+ try {
1305
+ parsedPayload = JSON.parse(r.payload);
1306
+ } catch {
1307
+ parsedPayload = r.payload;
1308
+ }
1309
+ return {
1310
+ id: r.id,
1311
+ publicId: r.public_id,
1312
+ queue: r.queue,
1313
+ status: r.status,
1314
+ priority: r.priority,
1315
+ attempts: r.attempts,
1316
+ maxAttempts: r.max_attempts,
1317
+ runAt: r.run_at,
1318
+ createdAt: r.created_at,
1319
+ updatedAt: r.updated_at,
1320
+ lastError: r.last_error,
1321
+ payload: parsedPayload
1322
+ };
1323
+ });
1324
+ return { jobs, count: jobs.length, limit, offset };
1325
+ }
1326
+ case "workmatic_get_dead_jobs": {
1327
+ const queue = args2.queue;
1328
+ const limit = Math.min(Math.max(Number(args2.limit ?? 20), 1), 100);
1329
+ return executeTool(db, "workmatic_list_jobs", {
1330
+ queue,
1331
+ status: "dead",
1332
+ limit
1333
+ });
1334
+ }
1335
+ case "workmatic_add_job": {
1336
+ const queue = args2.queue || "default";
1337
+ const payload = args2.payload;
1338
+ const priority = args2.priority !== void 0 ? Number(args2.priority) : 0;
1339
+ const delayMs = args2.delayMs !== void 0 ? Number(args2.delayMs) : 0;
1340
+ const maxAttempts = args2.maxAttempts !== void 0 ? Number(args2.maxAttempts) : 3;
1341
+ const client = createClient({ db, queue });
1342
+ const result = await client.add(payload, { priority, delayMs, maxAttempts });
1343
+ return { ok: true, id: result.id, queue };
1344
+ }
1345
+ case "workmatic_retry_job": {
1346
+ const publicId = args2.publicId;
1347
+ if (!publicId) {
1348
+ throw new Error("publicId is required");
1349
+ }
1350
+ const job = await db.selectFrom("workmatic_jobs").select(["queue", "status"]).where("public_id", "=", publicId).executeTakeFirst();
1351
+ if (!job) {
1352
+ throw new Error(`Job not found: ${publicId}`);
1353
+ }
1354
+ const timestamp = now();
1355
+ await db.updateTable("workmatic_jobs").set({
1356
+ status: "ready",
1357
+ attempts: 0,
1358
+ lease_until: 0,
1359
+ last_error: null,
1360
+ run_at: timestamp,
1361
+ updated_at: timestamp
1362
+ }).where("public_id", "=", publicId).execute();
1363
+ context?.onJobStatusChanged?.({
1364
+ publicId,
1365
+ queue: job.queue,
1366
+ previousStatus: job.status,
1367
+ status: "ready",
1368
+ timestamp,
1369
+ error: null
1370
+ });
1371
+ return { ok: true, id: publicId, message: `Job ${publicId} reset to ready` };
1372
+ }
1373
+ case "workmatic_retry_all_dead": {
1374
+ const queue = args2.queue;
1375
+ const timestamp = now();
1376
+ let query = db.updateTable("workmatic_jobs").set({
1377
+ status: "ready",
1378
+ attempts: 0,
1379
+ lease_until: 0,
1380
+ last_error: null,
1381
+ run_at: timestamp,
1382
+ updated_at: timestamp
1383
+ }).where("status", "=", "dead");
1384
+ if (queue) {
1385
+ query = query.where("queue", "=", queue);
1386
+ }
1387
+ const res = await query.execute();
1388
+ const retriedCount = Number(res[0].numUpdatedRows);
1389
+ if (retriedCount > 0) {
1390
+ context?.onJobStatusChanged?.({
1391
+ publicId: "*",
1392
+ queue: queue ?? "*",
1393
+ previousStatus: "dead",
1394
+ status: "ready",
1395
+ timestamp,
1396
+ error: null
1397
+ });
1398
+ }
1399
+ return { ok: true, retriedCount, queue: queue ?? "all" };
1400
+ }
1401
+ case "workmatic_update_job_status": {
1402
+ const publicId = args2.publicId;
1403
+ const status = args2.status;
1404
+ if (!publicId) {
1405
+ throw new Error("publicId is required");
1406
+ }
1407
+ if (!status || !["ready", "done", "dead"].includes(status)) {
1408
+ throw new Error("status is required and must be 'ready', 'done', or 'dead'");
1409
+ }
1410
+ const job = await db.selectFrom("workmatic_jobs").select(["id", "public_id", "queue", "status", "attempts"]).where("public_id", "=", publicId).executeTakeFirst();
1411
+ if (!job) {
1412
+ throw new Error(`Job not found: ${publicId}`);
1413
+ }
1414
+ if (job.status === status) {
1415
+ return {
1416
+ ok: true,
1417
+ id: publicId,
1418
+ queue: job.queue,
1419
+ status,
1420
+ unchanged: true
1421
+ };
1422
+ }
1423
+ const timestamp = now();
1424
+ const previousStatus = job.status;
1425
+ const delayMs = Math.max(Number(args2.delayMs ?? 0), 0);
1426
+ const resetAttempts = args2.resetAttempts !== void 0 ? Boolean(args2.resetAttempts) : status === "ready";
1427
+ const updateData = {
1428
+ status,
1429
+ updated_at: timestamp,
1430
+ lease_until: 0
1431
+ };
1432
+ if (status === "ready") {
1433
+ updateData.run_at = timestamp + delayMs;
1434
+ }
1435
+ if (resetAttempts) {
1436
+ updateData.attempts = 0;
1437
+ }
1438
+ if (args2.error !== void 0) {
1439
+ updateData.last_error = args2.error;
1440
+ } else if (status === "ready") {
1441
+ updateData.last_error = null;
1442
+ }
1443
+ await db.updateTable("workmatic_jobs").set(updateData).where("public_id", "=", publicId).execute();
1444
+ const event = {
1445
+ publicId,
1446
+ queue: job.queue,
1447
+ previousStatus,
1448
+ status,
1449
+ timestamp,
1450
+ error: updateData.last_error ?? null
1451
+ };
1452
+ context?.onJobStatusChanged?.(event);
1453
+ return {
1454
+ ok: true,
1455
+ id: publicId,
1456
+ queue: job.queue,
1457
+ previousStatus,
1458
+ status,
1459
+ signaled: true
1460
+ };
1461
+ }
1462
+ case "workmatic_pause_queue": {
1463
+ const queue = args2.queue;
1464
+ if (!queue) {
1465
+ throw new Error("queue is required");
1466
+ }
1467
+ const timestamp = now();
1468
+ await import_kysely5.sql`
1469
+ INSERT INTO workmatic_settings (queue, paused, updated_at)
1470
+ VALUES (${queue}, 1, ${timestamp})
1471
+ ON CONFLICT(queue) DO UPDATE SET
1472
+ paused = 1,
1473
+ updated_at = ${timestamp}
1474
+ `.execute(db);
1475
+ return { ok: true, queue, paused: true };
1476
+ }
1477
+ case "workmatic_resume_queue": {
1478
+ const queue = args2.queue;
1479
+ if (!queue) {
1480
+ throw new Error("queue is required");
1481
+ }
1482
+ const timestamp = now();
1483
+ await import_kysely5.sql`
1484
+ INSERT INTO workmatic_settings (queue, paused, updated_at)
1485
+ VALUES (${queue}, 0, ${timestamp})
1486
+ ON CONFLICT(queue) DO UPDATE SET
1487
+ paused = 0,
1488
+ updated_at = ${timestamp}
1489
+ `.execute(db);
1490
+ return { ok: true, queue, paused: false };
1491
+ }
1492
+ case "workmatic_purge_jobs": {
1493
+ const queue = args2.queue;
1494
+ const status = args2.status || "done";
1495
+ let query = db.deleteFrom("workmatic_jobs");
1496
+ if (queue) {
1497
+ query = query.where("queue", "=", queue);
1498
+ }
1499
+ if (status !== "all") {
1500
+ query = query.where("status", "=", status);
1501
+ }
1502
+ const res = await query.execute();
1503
+ const deletedCount = Number(res[0].numDeletedRows);
1504
+ return { ok: true, deletedCount, queue: queue ?? "all", status };
1505
+ }
1506
+ case "workmatic_transfer_jobs": {
1507
+ const fromQueue = args2.fromQueue;
1508
+ const toQueue = args2.toQueue;
1509
+ if (!fromQueue || !toQueue) {
1510
+ throw new Error("fromQueue and toQueue are required");
1511
+ }
1512
+ if (fromQueue === toQueue) {
1513
+ return { ok: true, moved: 0 };
1514
+ }
1515
+ const status = args2.status || "ready";
1516
+ const limit = Math.max(Number(args2.limit ?? 1e3), 1);
1517
+ const resetForRetry = Boolean(args2.resetForRetry);
1518
+ const orch = createOrchestrator({ db });
1519
+ const result = await orch.transfer({
1520
+ from: fromQueue,
1521
+ to: toQueue,
1522
+ status,
1523
+ limit,
1524
+ resetForRetry
1525
+ });
1526
+ return {
1527
+ ok: true,
1528
+ moved: result.moved,
1529
+ fromQueue,
1530
+ toQueue,
1531
+ status: resetForRetry && status === "dead" ? "ready" : status
1532
+ };
1533
+ }
1534
+ default:
1535
+ throw new Error(`Unknown tool: ${name}`);
1536
+ }
1537
+ }
1538
+
1539
+ // src/mcp/server.ts
1540
+ function createMcpServer(options) {
1541
+ const db = options.db;
1542
+ const input = options.input ?? process.stdin;
1543
+ const output = options.output ?? process.stdout;
1544
+ const emitter = new import_node_events.EventEmitter();
1545
+ let rl = null;
1546
+ let isRunning = false;
1547
+ function notifyJobStatusChanged(event) {
1548
+ if (options.onJobStatusChanged) {
1549
+ options.onJobStatusChanged(event);
1550
+ }
1551
+ emitter.emit("jobStatusChanged", event);
1552
+ if (event.status === "ready") {
1553
+ if (options.orchestrator) {
1554
+ if (event.queue === "*") {
1555
+ for (const worker of options.orchestrator.workers()) {
1556
+ worker.wakeUp();
1557
+ }
1558
+ } else {
1559
+ try {
1560
+ options.orchestrator.worker(event.queue).wakeUp();
1561
+ } catch {
1562
+ }
1563
+ }
1564
+ }
1565
+ if (options.workers) {
1566
+ for (const worker of options.workers) {
1567
+ if (event.queue === "*" || worker.queue === event.queue) {
1568
+ worker.wakeUp();
1569
+ }
1570
+ }
1571
+ }
1572
+ }
1573
+ if (isRunning) {
1574
+ output.write(
1575
+ JSON.stringify({
1576
+ jsonrpc: "2.0",
1577
+ method: "notifications/workmatic/job_status_changed",
1578
+ params: event
1579
+ }) + "\n"
1580
+ );
1581
+ }
1582
+ }
1583
+ async function handleMessage(raw) {
1584
+ const trimmed = raw.trim();
1585
+ if (!trimmed) {
1586
+ return null;
1587
+ }
1588
+ let msg;
1589
+ try {
1590
+ msg = JSON.parse(trimmed);
1591
+ } catch {
1592
+ return JSON.stringify({
1593
+ jsonrpc: "2.0",
1594
+ id: null,
1595
+ error: {
1596
+ code: -32700,
1597
+ message: "Parse error"
1598
+ }
1599
+ });
1600
+ }
1601
+ if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0" || typeof msg.method !== "string") {
1602
+ return JSON.stringify({
1603
+ jsonrpc: "2.0",
1604
+ id: msg?.id ?? null,
1605
+ error: {
1606
+ code: -32600,
1607
+ message: "Invalid Request"
1608
+ }
1609
+ });
1610
+ }
1611
+ const isNotification = msg.id === void 0;
1612
+ switch (msg.method) {
1613
+ case "initialize": {
1614
+ const result = {
1615
+ protocolVersion: "2024-11-05",
1616
+ capabilities: {
1617
+ tools: {}
1618
+ },
1619
+ serverInfo: {
1620
+ name: "workmatic-mcp",
1621
+ version: "0.1.0"
1622
+ }
1623
+ };
1624
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
1625
+ }
1626
+ case "notifications/initialized": {
1627
+ return null;
1628
+ }
1629
+ case "ping": {
1630
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: {} });
1631
+ }
1632
+ case "tools/list": {
1633
+ const result = {
1634
+ tools: MCP_TOOL_DEFINITIONS
1635
+ };
1636
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
1637
+ }
1638
+ case "tools/call": {
1639
+ if (!msg.params || typeof msg.params.name !== "string") {
1640
+ return isNotification ? null : JSON.stringify({
1641
+ jsonrpc: "2.0",
1642
+ id: msg.id,
1643
+ error: {
1644
+ code: -32602,
1645
+ message: 'Invalid params: tool "name" is required'
1646
+ }
1647
+ });
1648
+ }
1649
+ const toolName = msg.params.name;
1650
+ const toolArgs = msg.params.arguments ?? {};
1651
+ try {
1652
+ const toolResult = await executeTool(db, toolName, toolArgs, {
1653
+ onJobStatusChanged: notifyJobStatusChanged
1654
+ });
1655
+ const response = {
1656
+ content: [
1657
+ {
1658
+ type: "text",
1659
+ text: JSON.stringify(toolResult, null, 2)
1660
+ }
1661
+ ]
1662
+ };
1663
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: response });
1664
+ } catch (err) {
1665
+ const errorMessage = err instanceof Error ? err.message : String(err);
1666
+ const response = {
1667
+ content: [
1668
+ {
1669
+ type: "text",
1670
+ text: errorMessage
1671
+ }
1672
+ ],
1673
+ isError: true
1674
+ };
1675
+ return isNotification ? null : JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: response });
1676
+ }
1677
+ }
1678
+ default: {
1679
+ return isNotification ? null : JSON.stringify({
1680
+ jsonrpc: "2.0",
1681
+ id: msg.id,
1682
+ error: {
1683
+ code: -32601,
1684
+ message: `Method not found: ${msg.method}`
1685
+ }
1686
+ });
1687
+ }
1688
+ }
1689
+ }
1690
+ const server = {
1691
+ start() {
1692
+ if (isRunning) return;
1693
+ isRunning = true;
1694
+ rl = import_node_readline.default.createInterface({
1695
+ input,
1696
+ terminal: false
1697
+ });
1698
+ rl.on("line", (line) => {
1699
+ void handleMessage(line).then((response) => {
1700
+ if (response && isRunning) {
1701
+ output.write(response + "\n");
1702
+ }
1703
+ });
1704
+ });
1705
+ },
1706
+ stop() {
1707
+ if (!isRunning) return;
1708
+ isRunning = false;
1709
+ rl.close();
1710
+ rl = null;
1711
+ },
1712
+ handleMessage,
1713
+ on(event, listener) {
1714
+ emitter.on(event, listener);
1715
+ return server;
1716
+ },
1717
+ off(event, listener) {
1718
+ emitter.off(event, listener);
1719
+ return server;
1720
+ }
1721
+ };
1722
+ return server;
1723
+ }
1724
+
769
1725
  // src/cli/handlers.ts
770
1726
  function printUsage() {
771
1727
  console.log(`
@@ -785,6 +1741,8 @@ Commands:
785
1741
  pause <db> <queue> Pause a queue (running workers stop claiming)
786
1742
  resume <db> <queue> Resume a paused queue
787
1743
  transfer <db> <from> <to> Move jobs between queues
1744
+ mcp <db> Start Model Context Protocol (MCP) server
1745
+
788
1746
 
789
1747
  Options:
790
1748
  --status=<status> Filter by status (ready|running|done|dead), comma-separated for transfer
@@ -1258,6 +2216,34 @@ async function cmdTransfer(dbPath, from, to, options) {
1258
2216
  await db.destroy();
1259
2217
  }
1260
2218
  }
2219
+ async function cmdMcp(dbPath, options = {}) {
2220
+ const db = createDatabase({ filename: dbPath });
2221
+ const server = createMcpServer({
2222
+ db,
2223
+ input: options.input,
2224
+ output: options.output
2225
+ });
2226
+ server.start();
2227
+ await new Promise((resolve) => {
2228
+ const cleanup = () => {
2229
+ server.stop();
2230
+ if (!options.input) {
2231
+ process.removeListener("SIGINT", cleanup);
2232
+ process.removeListener("SIGTERM", cleanup);
2233
+ process.stdin.removeListener("end", cleanup);
2234
+ }
2235
+ resolve();
2236
+ };
2237
+ if (options.input) {
2238
+ options.input.once("end", cleanup);
2239
+ } else {
2240
+ process.stdin.once("end", cleanup);
2241
+ process.once("SIGINT", cleanup);
2242
+ process.once("SIGTERM", cleanup);
2243
+ }
2244
+ });
2245
+ await db.destroy();
2246
+ }
1261
2247
  async function runCommand(command2, dbPath, positionalArgs, options) {
1262
2248
  switch (command2) {
1263
2249
  case "stats":
@@ -1302,6 +2288,9 @@ async function runCommand(command2, dbPath, positionalArgs, options) {
1302
2288
  }
1303
2289
  await cmdTransfer(dbPath, positionalArgs[1], positionalArgs[2], options);
1304
2290
  break;
2291
+ case "mcp":
2292
+ await cmdMcp(dbPath);
2293
+ break;
1305
2294
  default:
1306
2295
  throw new Error(`Unknown command: ${command2}`);
1307
2296
  }
@@ -1320,7 +2309,8 @@ var KNOWN_COMMANDS = [
1320
2309
  "pause",
1321
2310
  "resume",
1322
2311
  "queues",
1323
- "transfer"
2312
+ "transfer",
2313
+ "mcp"
1324
2314
  ];
1325
2315
  function isCliCommand(value) {
1326
2316
  return KNOWN_COMMANDS.includes(value);