agent-pool-mcp 1.0.0 → 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/src/server.js CHANGED
@@ -17,6 +17,8 @@ import { getSystemLoad } from './runner/process-manager.js';
17
17
  import { createTask, completeTask, failTask, formatTaskResult, getActiveTasks, cancelTask } from './tools/results.js';
18
18
  import { listSkills, createSkill, deleteSkill, installSkill, provisionSkill } from './tools/skills.js';
19
19
  import { consultPeer } from './tools/consult.js';
20
+ import { addSchedule, listSchedules, removeSchedule, getScheduledResults, getDaemonStatus } from './scheduler/scheduler.js';
21
+ import { createPipeline, listPipelines, runPipeline, getRun, listRuns, cancelRun, signalStepComplete, bounceBack } from './scheduler/pipeline.js';
20
22
 
21
23
  import { TOOL_DEFINITIONS } from './tool-definitions.js';
22
24
 
@@ -105,7 +107,7 @@ export function createServer() {
105
107
  }
106
108
 
107
109
  const server = new Server(
108
- { name: 'agent-pool', version: '1.0.0' },
110
+ { name: 'agent-pool', version: '1.2.0' },
109
111
  { capabilities: { tools: {} } },
110
112
  );
111
113
 
@@ -145,6 +147,28 @@ export function createServer() {
145
147
  response = handleDeleteSkill(args); break;
146
148
  case 'install_skill':
147
149
  response = handleInstallSkill(args); break;
150
+ case 'schedule_task':
151
+ response = handleScheduleTask(args); break;
152
+ case 'list_schedules':
153
+ response = handleListSchedules(args); break;
154
+ case 'cancel_schedule':
155
+ response = handleCancelSchedule(args); break;
156
+ case 'get_scheduled_results':
157
+ response = handleGetScheduledResults(args); break;
158
+ case 'create_pipeline':
159
+ response = handleCreatePipeline(args); break;
160
+ case 'run_pipeline':
161
+ response = handleRunPipeline(args); break;
162
+ case 'list_pipelines':
163
+ response = handleListPipelines(args); break;
164
+ case 'get_pipeline_status':
165
+ response = handleGetPipelineStatus(args); break;
166
+ case 'cancel_pipeline':
167
+ response = handleCancelPipeline(args); break;
168
+ case 'signal_step_complete':
169
+ response = handleSignalStepComplete(args); break;
170
+ case 'bounce_back':
171
+ response = handleBounceBack(args); break;
148
172
  default:
149
173
  response = { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
150
174
  }
@@ -348,3 +372,247 @@ function handleInstallSkill(args) {
348
372
  }],
349
373
  };
350
374
  }
375
+
376
+ // ─── Scheduler Handlers ─────────────────────────────────────────────
377
+
378
+ /** @param {object} args */
379
+ function handleScheduleTask(args) {
380
+ const cwd = args.cwd ?? defaultCwd;
381
+ try {
382
+ const result = addSchedule(cwd, {
383
+ prompt: args.prompt,
384
+ cron: args.cron,
385
+ skill: args.skill,
386
+ approvalMode: args.approval_mode,
387
+ catchup: args.catchup,
388
+ taskCwd: args.cwd,
389
+ });
390
+
391
+ return {
392
+ content: [{
393
+ type: 'text',
394
+ text: `⏰ Task scheduled.\n\n- **Schedule ID**: \`${result.scheduleId}\`\n- **Cron**: \`${args.cron}\`\n- **Next run**: ${result.nextRun || 'unknown'}\n- **Prompt**: ${args.prompt.substring(0, 100)}...\n\nDaemon is running in the background. Results will be saved to \`.agent/scheduled-results/\`.\nUse \`list_schedules\` to see all schedules, \`get_scheduled_results\` to read outputs.`,
395
+ }],
396
+ };
397
+ } catch (error) {
398
+ return { content: [{ type: 'text', text: `❌ Failed to schedule: ${error.message}` }], isError: true };
399
+ }
400
+ }
401
+
402
+ /** @param {object} args */
403
+ function handleListSchedules(args) {
404
+ const cwd = args.cwd ?? defaultCwd;
405
+ const schedules = listSchedules(cwd);
406
+ const daemon = getDaemonStatus(cwd);
407
+
408
+ if (schedules.length === 0) {
409
+ return { content: [{ type: 'text', text: `No scheduled tasks.\n\nDaemon: ${daemon.running ? `running (pid ${daemon.pid})` : 'not running'}` }] };
410
+ }
411
+
412
+ const lines = schedules.map((s) =>
413
+ `- **${s.id}** | \`${s.cron}\` | next: ${s.nextRun || '?'} | last: ${s.lastRun || 'never'}\n ${s.prompt.substring(0, 80)}`,
414
+ );
415
+
416
+ return {
417
+ content: [{
418
+ type: 'text',
419
+ text: `## Scheduled Tasks (${schedules.length})\n\nDaemon: ${daemon.running ? `✅ running (pid ${daemon.pid})` : '❌ not running'}\n\n${lines.join('\n')}`,
420
+ }],
421
+ };
422
+ }
423
+
424
+ /** @param {object} args */
425
+ function handleCancelSchedule(args) {
426
+ const cwd = args.cwd ?? defaultCwd;
427
+ const removed = removeSchedule(cwd, args.schedule_id);
428
+ return {
429
+ content: [{
430
+ type: 'text',
431
+ text: removed
432
+ ? `✅ Schedule \`${args.schedule_id}\` cancelled. Daemon will auto-exit when no schedules remain.`
433
+ : `❌ Schedule \`${args.schedule_id}\` not found.`,
434
+ }],
435
+ };
436
+ }
437
+
438
+ /** @param {object} args */
439
+ function handleGetScheduledResults(args) {
440
+ const cwd = args.cwd ?? defaultCwd;
441
+ const results = getScheduledResults(cwd, args.schedule_id);
442
+
443
+ if (results.length === 0) {
444
+ return { content: [{ type: 'text', text: 'No scheduled results yet.' }] };
445
+ }
446
+
447
+ const lines = results.map((r) =>
448
+ `### ${r.scheduleId} — ${r.executedAt}\n- Exit: ${r.exitCode}\n- Events: ${r.totalEvents}\n\n\`\`\`\n${(r.response || '').substring(0, 500)}\n\`\`\``,
449
+ );
450
+
451
+ return {
452
+ content: [{
453
+ type: 'text',
454
+ text: `## Scheduled Results (${results.length})\n\n${lines.join('\n\n')}`,
455
+ }],
456
+ };
457
+ }
458
+
459
+ // ─── Pipeline Handlers ─────────────────────────────────────────────
460
+
461
+ /** @param {object} args */
462
+ function handleCreatePipeline(args) {
463
+ const cwd = args.cwd ?? defaultCwd;
464
+ const result = createPipeline(cwd, args);
465
+ return {
466
+ content: [{
467
+ type: 'text',
468
+ text: `✅ Pipeline created.\n\n- **Pipeline ID**: \`${result.pipelineId}\`\n- **Steps**: ${args.steps.length}`
469
+ }],
470
+ };
471
+ }
472
+
473
+ /** @param {object} args */
474
+ function handleRunPipeline(args) {
475
+ const cwd = args.cwd ?? defaultCwd;
476
+ const result = runPipeline(cwd, args.pipeline_id);
477
+ if (!result) {
478
+ return {
479
+ content: [{ type: 'text', text: `❌ Pipeline \`${args.pipeline_id}\` not found.` }],
480
+ isError: true,
481
+ };
482
+ }
483
+ return {
484
+ content: [{
485
+ type: 'text',
486
+ text: `🚀 Pipeline started.\n\n- **Run ID**: \`${result.runId}\``
487
+ }],
488
+ };
489
+ }
490
+
491
+ /** @param {object} args */
492
+ function handleListPipelines(args) {
493
+ const cwd = args.cwd ?? defaultCwd;
494
+ const pipelines = listPipelines(cwd);
495
+ const runs = listRuns(cwd);
496
+
497
+ const parts = [];
498
+
499
+ if (pipelines.length > 0) {
500
+ const lines = pipelines.map(p => `- **${p.name}** (\`${p.id}\`) — ${p.steps.length} steps`);
501
+ parts.push(`## Pipelines (${pipelines.length})\n\n${lines.join('\n')}`);
502
+ }
503
+
504
+ const activeRuns = runs.filter(r => r.status === 'running');
505
+ const recentRuns = runs.filter(r => r.status !== 'running').slice(0, 5);
506
+
507
+ if (activeRuns.length > 0) {
508
+ const emojiMap = { success: '✅', failed: '❌', running: '🔄', pending: '⏸️', bounce_pending: '↩️', waiting_bounce: '⏳', skipped: '⏭️', cancelled: '🛑' };
509
+ const lines = activeRuns.map(r => {
510
+ const stepsSummary = Object.entries(r.steps).map(([n, s]) => `${emojiMap[s.status] || '❓'}${n}`).join(' → ');
511
+ return `- \`${r.id}\` (${r.pipelineName}) ${stepsSummary}`;
512
+ });
513
+ parts.push(`## Active Runs (${activeRuns.length})\n\n${lines.join('\n')}`);
514
+ }
515
+
516
+ if (recentRuns.length > 0) {
517
+ const lines = recentRuns.map(r => `- \`${r.id}\` ${r.status === 'success' ? '✅' : '❌'} ${r.pipelineName} (${r.completedAt || ''})`);
518
+ parts.push(`## Recent Runs\n\n${lines.join('\n')}`);
519
+ }
520
+
521
+ if (parts.length === 0) {
522
+ return { content: [{ type: 'text', text: 'No pipelines found.' }] };
523
+ }
524
+
525
+ return {
526
+ content: [{ type: 'text', text: parts.join('\n\n') }],
527
+ };
528
+ }
529
+
530
+ /** @param {object} args */
531
+ function handleGetPipelineStatus(args) {
532
+ const cwd = args.cwd ?? defaultCwd;
533
+ const run = getRun(cwd, args.run_id);
534
+
535
+ if (!run) {
536
+ return { content: [{ type: 'text', text: `❌ Pipeline run \`${args.run_id}\` not found.` }], isError: true };
537
+ }
538
+
539
+ const emojiMap = {
540
+ success: '✅',
541
+ failed: '❌',
542
+ running: '🔄',
543
+ pending: '⏸️',
544
+ bounce_pending: '↩️',
545
+ waiting_bounce: '⏳',
546
+ skipped: '⏭️',
547
+ cancelled: '🛑',
548
+ };
549
+
550
+ const lines = Object.entries(run.steps).map(([name, s]) => {
551
+ const emoji = emojiMap[s.status] || s.status;
552
+ return `- ${emoji} **${name}**: ${s.status}`;
553
+ });
554
+
555
+ return {
556
+ content: [{
557
+ type: 'text',
558
+ text: `## Pipeline Status: \`${args.run_id}\`\n**Status**: ${run.status}\n\n${lines.join('\n')}`
559
+ }],
560
+ };
561
+ }
562
+
563
+ /** @param {object} args */
564
+ function handleCancelPipeline(args) {
565
+ const cwd = args.cwd ?? defaultCwd;
566
+ const success = cancelRun(cwd, args.run_id);
567
+ return {
568
+ content: [{
569
+ type: 'text',
570
+ text: success ? `✅ Pipeline \`${args.run_id}\` cancelled.` : `❌ Failed to cancel pipeline \`${args.run_id}\` (not found or not running).`
571
+ }],
572
+ };
573
+ }
574
+
575
+ /** @param {object} args */
576
+ function handleSignalStepComplete(args) {
577
+ const cwd = args.cwd ?? defaultCwd;
578
+ const result = signalStepComplete(cwd, args.step_name, args.output, args.run_id);
579
+ if (result.success) {
580
+ return {
581
+ content: [{
582
+ type: 'text',
583
+ text: `✅ Step \`${args.step_name}\` marked as complete.`
584
+ }],
585
+ };
586
+ } else {
587
+ return {
588
+ content: [{
589
+ type: 'text',
590
+ text: `❌ Failed to signal step completion. Step might not be running or run not found.`
591
+ }],
592
+ isError: true,
593
+ };
594
+ }
595
+ }
596
+
597
+ /** @param {object} args */
598
+ function handleBounceBack(args) {
599
+ const cwd = args.cwd ?? defaultCwd;
600
+ const info = bounceBack(cwd, args.step_name, args.reason, args.run_id);
601
+ if (info.success) {
602
+ return {
603
+ content: [{
604
+ type: 'text',
605
+ text: `↩️ Task bounced back to \`${args.step_name}\`.\nReason: ${args.reason}\nBounces: ${info.bounceCount}/${info.maxBounces}`
606
+ }],
607
+ };
608
+ } else {
609
+ return {
610
+ content: [{
611
+ type: 'text',
612
+ text: `❌ Failed to bounce back. Pipeline might have reached max bounces or step not found.`
613
+ }],
614
+ isError: true,
615
+ };
616
+ }
617
+ }
618
+
@@ -173,4 +173,172 @@ export const TOOL_DEFINITIONS = [
173
173
  required: ['skill_name'],
174
174
  },
175
175
  },
176
+ // ─── Scheduler Tools ────────────────────────────────────
177
+ {
178
+ name: 'schedule_task',
179
+ description: [
180
+ 'Schedule a Gemini CLI agent to run on a cron schedule or as a delayed one-shot.',
181
+ 'Spawns a persistent daemon that survives IDE/CLI restarts.',
182
+ 'Results are saved to .agent/scheduled-results/ and can be retrieved with get_scheduled_results.',
183
+ '',
184
+ 'Cron format: standard 5-field (minute hour day month weekday).',
185
+ 'Examples: "*/30 * * * *" (every 30 min), "0 9 * * MON-FRI" (9am weekdays), "0 */2 * * *" (every 2 hours).',
186
+ ].join('\n'),
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ prompt: { type: 'string', description: 'Task prompt for the Gemini agent.' },
191
+ cron: { type: 'string', description: 'Cron expression (5-field). E.g. "0 9 * * *" for daily at 9am.' },
192
+ cwd: { type: 'string', description: 'Working directory for the scheduled task. Defaults to current directory.' },
193
+ skill: { type: 'string', description: 'Skill to activate for each run.' },
194
+ approval_mode: {
195
+ type: 'string',
196
+ enum: ['yolo', 'auto_edit', 'plan'],
197
+ description: 'Approval mode for scheduled runs. Default: yolo.',
198
+ },
199
+ catchup: { type: 'boolean', description: 'If true, run missed schedules on daemon restart. Default: false (skip missed).' },
200
+ },
201
+ required: ['prompt', 'cron'],
202
+ },
203
+ },
204
+ {
205
+ name: 'list_schedules',
206
+ description: 'List all scheduled tasks with their cron expressions, next run times, and daemon status.',
207
+ inputSchema: {
208
+ type: 'object',
209
+ properties: {
210
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
211
+ },
212
+ },
213
+ },
214
+ {
215
+ name: 'cancel_schedule',
216
+ description: 'Cancel a scheduled task by ID. Removes it from the schedule. The daemon will auto-exit when no schedules remain.',
217
+ inputSchema: {
218
+ type: 'object',
219
+ properties: {
220
+ schedule_id: { type: 'string', description: 'Schedule ID to cancel.' },
221
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
222
+ },
223
+ required: ['schedule_id'],
224
+ },
225
+ },
226
+ {
227
+ name: 'get_scheduled_results',
228
+ description: 'Get results from scheduled task executions. Returns the last 20 results, newest first.',
229
+ inputSchema: {
230
+ type: 'object',
231
+ properties: {
232
+ schedule_id: { type: 'string', description: 'Filter results by schedule ID. Omit to get all.' },
233
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
234
+ },
235
+ },
236
+ },
237
+ // ─── Pipeline Tools ────────────────────────────────────
238
+ {
239
+ name: 'create_pipeline',
240
+ description: 'Create a pipeline definition with sequential steps.',
241
+ inputSchema: {
242
+ type: 'object',
243
+ properties: {
244
+ name: { type: 'string', description: 'Pipeline name.' },
245
+ steps: {
246
+ type: 'array',
247
+ items: {
248
+ type: 'object',
249
+ properties: {
250
+ name: { type: 'string', description: 'Step name.' },
251
+ prompt: { type: 'string', description: 'Step prompt.' },
252
+ trigger: { type: 'string', description: 'Trigger condition.' },
253
+ skill: { type: 'string', description: 'Skill to use.' },
254
+ approval_mode: { type: 'string', description: 'Approval mode.' },
255
+ timeout: { type: 'number', description: 'Timeout in seconds.' },
256
+ max_bounces: { type: 'number', description: 'Maximum bounces allowed.' },
257
+ expected_output: { type: 'string', description: 'Expected output description.' },
258
+ },
259
+ required: ['name', 'prompt'],
260
+ },
261
+ description: 'Array of pipeline steps.',
262
+ },
263
+ on_error: { type: 'string', enum: ['stop', 'skip'], description: 'What to do on error. Default is stop.' },
264
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
265
+ },
266
+ required: ['name', 'steps'],
267
+ },
268
+ },
269
+ {
270
+ name: 'run_pipeline',
271
+ description: 'Start executing a pipeline.',
272
+ inputSchema: {
273
+ type: 'object',
274
+ properties: {
275
+ pipeline_id: { type: 'string', description: 'Pipeline ID to run.' },
276
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
277
+ },
278
+ required: ['pipeline_id'],
279
+ },
280
+ },
281
+ {
282
+ name: 'list_pipelines',
283
+ description: 'List all pipeline definitions.',
284
+ inputSchema: {
285
+ type: 'object',
286
+ properties: {
287
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
288
+ },
289
+ },
290
+ },
291
+ {
292
+ name: 'get_pipeline_status',
293
+ description: 'Get status of a pipeline run.',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ run_id: { type: 'string', description: 'Pipeline run ID.' },
298
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
299
+ },
300
+ required: ['run_id'],
301
+ },
302
+ },
303
+ {
304
+ name: 'cancel_pipeline',
305
+ description: 'Cancel a running pipeline.',
306
+ inputSchema: {
307
+ type: 'object',
308
+ properties: {
309
+ run_id: { type: 'string', description: 'Pipeline run ID to cancel.' },
310
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
311
+ },
312
+ required: ['run_id'],
313
+ },
314
+ },
315
+ {
316
+ name: 'signal_step_complete',
317
+ description: 'Signal that a pipeline step is complete (called BY agents running inside pipeline steps).',
318
+ inputSchema: {
319
+ type: 'object',
320
+ properties: {
321
+ step_name: { type: 'string', description: 'Name of the completed step.' },
322
+ run_id: { type: 'string', description: 'Pipeline run ID for precise targeting. Provided in the task prompt.' },
323
+ output: { type: 'string', description: 'Optional output message or result.' },
324
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
325
+ },
326
+ required: ['step_name'],
327
+ },
328
+ },
329
+ {
330
+ name: 'bounce_back',
331
+ description: 'Return task to a previous pipeline step with feedback about missing/insufficient data.',
332
+ inputSchema: {
333
+ type: 'object',
334
+ properties: {
335
+ step_name: { type: 'string', description: 'Name of the step to return to.' },
336
+ reason: { type: 'string', description: 'Feedback about missing/insufficient data.' },
337
+ run_id: { type: 'string', description: 'Pipeline run ID for precise targeting. Provided in the task prompt.' },
338
+ cwd: { type: 'string', description: 'Project directory. Defaults to current working directory.' },
339
+ },
340
+ required: ['step_name', 'reason'],
341
+ },
342
+ },
176
343
  ];
344
+