mcp-rubber-duck 1.10.0 → 1.11.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +8 -0
  3. package/dist/providers/enhanced-manager.d.ts +7 -0
  4. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  5. package/dist/providers/enhanced-manager.js +36 -0
  6. package/dist/providers/enhanced-manager.js.map +1 -1
  7. package/dist/providers/manager.d.ts +1 -0
  8. package/dist/providers/manager.d.ts.map +1 -1
  9. package/dist/providers/manager.js +33 -0
  10. package/dist/providers/manager.js.map +1 -1
  11. package/dist/server.d.ts +1 -0
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +93 -33
  14. package/dist/server.js.map +1 -1
  15. package/dist/services/progress.d.ts +27 -0
  16. package/dist/services/progress.d.ts.map +1 -0
  17. package/dist/services/progress.js +50 -0
  18. package/dist/services/progress.js.map +1 -0
  19. package/dist/services/task-manager.d.ts +56 -0
  20. package/dist/services/task-manager.d.ts.map +1 -0
  21. package/dist/services/task-manager.js +134 -0
  22. package/dist/services/task-manager.js.map +1 -0
  23. package/dist/tools/compare-ducks.d.ts +2 -1
  24. package/dist/tools/compare-ducks.d.ts.map +1 -1
  25. package/dist/tools/compare-ducks.js +7 -3
  26. package/dist/tools/compare-ducks.js.map +1 -1
  27. package/dist/tools/duck-council.d.ts +2 -1
  28. package/dist/tools/duck-council.d.ts.map +1 -1
  29. package/dist/tools/duck-council.js +7 -3
  30. package/dist/tools/duck-council.js.map +1 -1
  31. package/dist/tools/duck-debate.d.ts +2 -1
  32. package/dist/tools/duck-debate.d.ts.map +1 -1
  33. package/dist/tools/duck-debate.js +19 -1
  34. package/dist/tools/duck-debate.js.map +1 -1
  35. package/dist/tools/duck-iterate.d.ts +2 -1
  36. package/dist/tools/duck-iterate.d.ts.map +1 -1
  37. package/dist/tools/duck-iterate.js +13 -1
  38. package/dist/tools/duck-iterate.js.map +1 -1
  39. package/dist/tools/duck-vote.d.ts +2 -1
  40. package/dist/tools/duck-vote.d.ts.map +1 -1
  41. package/dist/tools/duck-vote.js +7 -3
  42. package/dist/tools/duck-vote.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/providers/enhanced-manager.ts +49 -0
  45. package/src/providers/manager.ts +45 -0
  46. package/src/server.ts +110 -32
  47. package/src/services/progress.ts +59 -0
  48. package/src/services/task-manager.ts +162 -0
  49. package/src/tools/compare-ducks.ts +14 -3
  50. package/src/tools/duck-council.ts +15 -4
  51. package/src/tools/duck-debate.ts +31 -1
  52. package/src/tools/duck-iterate.ts +20 -1
  53. package/src/tools/duck-vote.ts +14 -3
  54. package/tests/duck-debate.test.ts +80 -0
  55. package/tests/duck-iterate.test.ts +81 -0
  56. package/tests/duck-vote.test.ts +70 -0
  57. package/tests/providers.test.ts +121 -0
  58. package/tests/services/progress.test.ts +137 -0
  59. package/tests/services/task-manager.test.ts +344 -0
  60. package/tests/tools/compare-ducks.test.ts +19 -0
  61. package/tests/tools/duck-council.test.ts +19 -0
@@ -1 +1 @@
1
- {"version":3,"file":"duck-vote.d.ts","sourceRoot":"","sources":["../../src/tools/duck-vote.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAK1D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,wBAAsB,YAAY,CAChC,eAAe,EAAE,eAAe,EAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;GAgG9B"}
1
+ {"version":3,"file":"duck-vote.d.ts","sourceRoot":"","sources":["../../src/tools/duck-vote.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,wBAAsB,YAAY,CAChC,eAAe,EAAE,eAAe,EAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,QAAQ,CAAC,EAAE,gBAAgB;;;;;GAyG5B"}
@@ -1,6 +1,6 @@
1
1
  import { ConsensusService } from '../services/consensus.js';
2
2
  import { logger } from '../utils/logger.js';
3
- export async function duckVoteTool(providerManager, args) {
3
+ export async function duckVoteTool(providerManager, args, progress) {
4
4
  const { question, options, voters, require_reasoning = true, } = args;
5
5
  // Validate inputs
6
6
  if (!question || typeof question !== 'string') {
@@ -22,8 +22,12 @@ export async function duckVoteTool(providerManager, args) {
22
22
  logger.info(`Starting vote with ${voterNames.length} voters on: "${question}"`);
23
23
  const consensusService = new ConsensusService();
24
24
  const votePrompt = consensusService.buildVotePrompt(question, options, require_reasoning);
25
- // Get votes from all ducks in parallel
26
- const responses = await providerManager.compareDucks(votePrompt, voterNames);
25
+ // Get votes from all ducks in parallel, reporting progress as each votes
26
+ const responses = progress
27
+ ? await providerManager.compareDucksWithProgress(votePrompt, voterNames, undefined, (providerName, completed, total) => {
28
+ void progress.report(completed, total, `${providerName} voted (${completed}/${total})`);
29
+ })
30
+ : await providerManager.compareDucks(votePrompt, voterNames);
27
31
  // Parse votes
28
32
  const votes = responses.map(response => {
29
33
  return consensusService.parseVote(response.content, response.provider, response.nickname, options);
@@ -1 +1 @@
1
- {"version":3,"file":"duck-vote.js","sourceRoot":"","sources":["../../src/tools/duck-vote.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"duck-vote.js","sourceRoot":"","sources":["../../src/tools/duck-vote.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-rubber-duck",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "An MCP server that bridges to multiple OpenAI-compatible LLMs - your AI rubber duck debugging panel",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -209,6 +209,55 @@ export class EnhancedProviderManager extends ProviderManager {
209
209
  return Promise.all(promises);
210
210
  }
211
211
 
212
+ async compareDucksWithProgressMCP(
213
+ prompt: string,
214
+ providerNames: string[] | undefined,
215
+ options: Partial<ChatOptions> | undefined,
216
+ onProviderComplete: (providerName: string, completed: number, total: number) => void
217
+ ): Promise<Array<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }>> {
218
+ if (!this.mcpEnabled) {
219
+ return this.compareDucksWithProgress(prompt, providerNames, options, onProviderComplete);
220
+ }
221
+
222
+ const providersToUse = providerNames
223
+ ? providerNames.map(name => this.enhancedProviders.get(name)).filter(Boolean)
224
+ : Array.from(this.enhancedProviders.values());
225
+
226
+ if (providersToUse.length === 0) {
227
+ throw new Error('No valid enhanced providers specified');
228
+ }
229
+
230
+ const total = providersToUse.length;
231
+ let completed = 0;
232
+
233
+ const promises = providersToUse.map(provider =>
234
+ provider ? this.askDuckWithMCP(provider.name, prompt, options)
235
+ .catch(error => ({
236
+ provider: provider.name,
237
+ nickname: provider.nickname,
238
+ model: '',
239
+ content: `Error: ${error instanceof Error ? error.message : String(error)}`,
240
+ latency: 0,
241
+ cached: false,
242
+ }))
243
+ .then(result => {
244
+ completed++;
245
+ onProviderComplete(provider.name, completed, total);
246
+ return result;
247
+ })
248
+ : Promise.resolve({
249
+ provider: 'unknown',
250
+ nickname: 'Unknown',
251
+ model: '',
252
+ content: 'Error: Invalid provider',
253
+ latency: 0,
254
+ cached: false,
255
+ })
256
+ );
257
+
258
+ return Promise.all(promises);
259
+ }
260
+
212
261
  async duckCouncilWithMCP(
213
262
  prompt: string,
214
263
  options?: Partial<ChatOptions>
@@ -183,6 +183,51 @@ export class ProviderManager {
183
183
  return Promise.all(promises);
184
184
  }
185
185
 
186
+ async compareDucksWithProgress(
187
+ prompt: string,
188
+ providerNames: string[] | undefined,
189
+ options: Partial<ChatOptions> | undefined,
190
+ onProviderComplete: (providerName: string, completed: number, total: number) => void
191
+ ): Promise<DuckResponse[]> {
192
+ const providersToUse = providerNames
193
+ ? providerNames.map(name => this.providers.get(name)).filter(Boolean)
194
+ : Array.from(this.providers.values());
195
+
196
+ if (providersToUse.length === 0) {
197
+ throw new Error('No valid providers specified');
198
+ }
199
+
200
+ const total = providersToUse.length;
201
+ let completed = 0;
202
+
203
+ const promises = providersToUse.map(provider =>
204
+ provider ? this.askDuck(provider.name, prompt, options)
205
+ .catch(error => ({
206
+ provider: provider.name,
207
+ nickname: provider.nickname,
208
+ model: '',
209
+ content: `Error: ${error instanceof Error ? error.message : String(error)}`,
210
+ latency: 0,
211
+ cached: false,
212
+ }))
213
+ .then(result => {
214
+ completed++;
215
+ onProviderComplete(provider.name, completed, total);
216
+ return result;
217
+ })
218
+ : Promise.resolve({
219
+ provider: 'unknown',
220
+ nickname: 'Unknown',
221
+ model: '',
222
+ content: 'Error: Invalid provider',
223
+ latency: 0,
224
+ cached: false,
225
+ })
226
+ );
227
+
228
+ return Promise.all(promises);
229
+ }
230
+
186
231
  async duckCouncil(
187
232
  prompt: string,
188
233
  options?: Partial<ChatOptions>
package/src/server.ts CHANGED
@@ -24,6 +24,8 @@ import { DuckResponse } from './config/types.js';
24
24
  import { ApprovalService } from './services/approval.js';
25
25
  import { FunctionBridge } from './services/function-bridge.js';
26
26
  import { GuardrailsService } from './guardrails/service.js';
27
+ import { TaskManager } from './services/task-manager.js';
28
+ import { createProgressReporter } from './services/progress.js';
27
29
  import { logger } from './utils/logger.js';
28
30
  import { duckArt, getRandomDuckMessage } from './utils/ascii-art.js';
29
31
 
@@ -69,13 +71,32 @@ export class RubberDuckServer {
69
71
  private functionBridge?: FunctionBridge;
70
72
  private mcpEnabled: boolean = false;
71
73
 
74
+ // Task management
75
+ private taskManager: TaskManager;
76
+
72
77
  constructor() {
78
+ this.taskManager = new TaskManager();
79
+
73
80
  this.server = new McpServer(
74
81
  {
75
82
  name: 'mcp-rubber-duck',
76
83
  version: '1.0.0',
77
84
  },
78
- {}
85
+ {
86
+ capabilities: {
87
+ tasks: {
88
+ list: {},
89
+ cancel: {},
90
+ requests: {
91
+ tools: { call: {} },
92
+ },
93
+ },
94
+ },
95
+ taskStore: this.taskManager.taskStore,
96
+ taskMessageQueue: this.taskManager.taskMessageQueue,
97
+ defaultTaskPollInterval: this.taskManager.config.pollInterval,
98
+ maxTaskQueueSize: this.taskManager.config.maxQueueSize,
99
+ }
79
100
  );
80
101
 
81
102
  // Initialize managers
@@ -313,12 +334,13 @@ export class RubberDuckServer {
313
334
  },
314
335
  _meta: { ui: { resourceUri: 'ui://rubber-duck/compare-ducks' } },
315
336
  },
316
- async (args) => {
337
+ async (args, extra) => {
317
338
  try {
339
+ const progress = createProgressReporter(extra._meta?.progressToken, extra.sendNotification);
318
340
  if (this.mcpEnabled && this.enhancedProviderManager) {
319
- return this.toolResult(await this.handleCompareDucksWithMCP(args as Record<string, unknown>));
341
+ return this.toolResult(await this.handleCompareDucksWithMCP(args as Record<string, unknown>, progress));
320
342
  }
321
- return this.toolResult(await compareDucksTool(this.providerManager, args as Record<string, unknown>));
343
+ return this.toolResult(await compareDucksTool(this.providerManager, args as Record<string, unknown>, progress));
322
344
  } catch (error) {
323
345
  return this.toolErrorResult(error);
324
346
  }
@@ -339,12 +361,13 @@ export class RubberDuckServer {
339
361
  openWorldHint: true,
340
362
  },
341
363
  },
342
- async (args) => {
364
+ async (args, extra) => {
343
365
  try {
366
+ const progress = createProgressReporter(extra._meta?.progressToken, extra.sendNotification);
344
367
  if (this.mcpEnabled && this.enhancedProviderManager) {
345
- return this.toolResult(await this.handleDuckCouncilWithMCP(args as Record<string, unknown>));
368
+ return this.toolResult(await this.handleDuckCouncilWithMCP(args as Record<string, unknown>, progress));
346
369
  }
347
- return this.toolResult(await duckCouncilTool(this.providerManager, args as Record<string, unknown>));
370
+ return this.toolResult(await duckCouncilTool(this.providerManager, args as Record<string, unknown>, progress));
348
371
  } catch (error) {
349
372
  return this.toolErrorResult(error);
350
373
  }
@@ -369,9 +392,10 @@ export class RubberDuckServer {
369
392
  },
370
393
  _meta: { ui: { resourceUri: 'ui://rubber-duck/duck-vote' } },
371
394
  },
372
- async (args) => {
395
+ async (args, extra) => {
373
396
  try {
374
- return this.toolResult(await duckVoteTool(this.providerManager, args as Record<string, unknown>));
397
+ const progress = createProgressReporter(extra._meta?.progressToken, extra.sendNotification);
398
+ return this.toolResult(await duckVoteTool(this.providerManager, args as Record<string, unknown>, progress));
375
399
  } catch (error) {
376
400
  return this.toolErrorResult(error);
377
401
  }
@@ -408,8 +432,8 @@ export class RubberDuckServer {
408
432
  }
409
433
  );
410
434
 
411
- // duck_iterate
412
- this.server.registerTool(
435
+ // duck_iterate (task-based: supports async execution for long-running iterations)
436
+ this.server.experimental.tasks.registerToolTask(
413
437
  'duck_iterate',
414
438
  {
415
439
  description: 'Iteratively refine a response between two ducks. One generates, the other critiques/improves, alternating for multiple rounds.',
@@ -423,19 +447,36 @@ export class RubberDuckServer {
423
447
  readOnlyHint: true,
424
448
  openWorldHint: true,
425
449
  },
450
+ execution: {
451
+ taskSupport: 'optional',
452
+ },
426
453
  },
427
- async (args) => {
428
- try {
429
- return this.toolResult(await duckIterateTool(this.providerManager, args as Record<string, unknown>));
430
- } catch (error) {
431
- return this.toolErrorResult(error);
432
- }
454
+ {
455
+ createTask: async (args, extra) => {
456
+ const task = await extra.taskStore.createTask({
457
+ ttl: this.taskManager.config.defaultTtl,
458
+ pollInterval: this.taskManager.config.pollInterval,
459
+ });
460
+ const progress = createProgressReporter(extra._meta?.progressToken, extra.sendNotification);
461
+ this.taskManager.startBackground(task.taskId, async (signal) => {
462
+ return this.toolResult(
463
+ await duckIterateTool(this.providerManager, args as Record<string, unknown>, progress, signal)
464
+ );
465
+ });
466
+ return { task };
467
+ },
468
+ getTask: async (_args, extra) => {
469
+ const task = await extra.taskStore.getTask(extra.taskId);
470
+ return task;
471
+ },
472
+ getTaskResult: async (_args, extra) => {
473
+ return await extra.taskStore.getTaskResult(extra.taskId) as CallToolResult;
474
+ },
433
475
  }
434
476
  );
435
477
 
436
- // duck_debate
437
- registerAppTool(
438
- this.server,
478
+ // duck_debate (task-based: supports async execution for multi-round debates)
479
+ this.server.experimental.tasks.registerToolTask(
439
480
  'duck_debate',
440
481
  {
441
482
  description: 'Structured multi-round debate between ducks. Supports oxford (pro/con), socratic (questioning), and adversarial (attack/defend) formats.',
@@ -451,13 +492,31 @@ export class RubberDuckServer {
451
492
  openWorldHint: true,
452
493
  },
453
494
  _meta: { ui: { resourceUri: 'ui://rubber-duck/duck-debate' } },
495
+ execution: {
496
+ taskSupport: 'optional',
497
+ },
454
498
  },
455
- async (args) => {
456
- try {
457
- return this.toolResult(await duckDebateTool(this.providerManager, args as Record<string, unknown>));
458
- } catch (error) {
459
- return this.toolErrorResult(error);
460
- }
499
+ {
500
+ createTask: async (args, extra) => {
501
+ const task = await extra.taskStore.createTask({
502
+ ttl: this.taskManager.config.defaultTtl,
503
+ pollInterval: this.taskManager.config.pollInterval,
504
+ });
505
+ const progress = createProgressReporter(extra._meta?.progressToken, extra.sendNotification);
506
+ this.taskManager.startBackground(task.taskId, async (signal) => {
507
+ return this.toolResult(
508
+ await duckDebateTool(this.providerManager, args as Record<string, unknown>, progress, signal)
509
+ );
510
+ });
511
+ return { task };
512
+ },
513
+ getTask: async (_args, extra) => {
514
+ const task = await extra.taskStore.getTask(extra.taskId);
515
+ return task;
516
+ },
517
+ getTaskResult: async (_args, extra) => {
518
+ return await extra.taskStore.getTaskResult(extra.taskId) as CallToolResult;
519
+ },
461
520
  }
462
521
  );
463
522
 
@@ -677,7 +736,7 @@ export class RubberDuckServer {
677
736
  };
678
737
  }
679
738
 
680
- private async handleCompareDucksWithMCP(args: Record<string, unknown>) {
739
+ private async handleCompareDucksWithMCP(args: Record<string, unknown>, progress?: import('./services/progress.js').ProgressReporter) {
681
740
  if (!this.enhancedProviderManager) {
682
741
  throw new Error('Enhanced provider manager not available');
683
742
  }
@@ -688,9 +747,16 @@ export class RubberDuckServer {
688
747
  model?: string;
689
748
  };
690
749
 
691
- const responses = await this.enhancedProviderManager.compareDucksWithMCP(prompt, providers, {
692
- model,
693
- });
750
+ const responses = progress
751
+ ? await this.enhancedProviderManager.compareDucksWithProgressMCP(
752
+ prompt,
753
+ providers,
754
+ { model },
755
+ (providerName, completed, total) => {
756
+ void progress.report(completed, total, `${providerName} responded (${completed}/${total})`);
757
+ }
758
+ )
759
+ : await this.enhancedProviderManager.compareDucksWithMCP(prompt, providers, { model });
694
760
 
695
761
  const formattedResponse = responses
696
762
  .map((response) => this.formatEnhancedDuckResponse(response))
@@ -726,14 +792,23 @@ export class RubberDuckServer {
726
792
  };
727
793
  }
728
794
 
729
- private async handleDuckCouncilWithMCP(args: Record<string, unknown>) {
795
+ private async handleDuckCouncilWithMCP(args: Record<string, unknown>, progress?: import('./services/progress.js').ProgressReporter) {
730
796
  if (!this.enhancedProviderManager) {
731
797
  throw new Error('Enhanced provider manager not available');
732
798
  }
733
799
 
734
800
  const { prompt, model } = args as { prompt: string; model?: string };
735
801
 
736
- const responses = await this.enhancedProviderManager.duckCouncilWithMCP(prompt, { model });
802
+ const responses = progress
803
+ ? await this.enhancedProviderManager.compareDucksWithProgressMCP(
804
+ prompt,
805
+ undefined,
806
+ { model },
807
+ (providerName, completed, total) => {
808
+ void progress.report(completed, total, `${providerName} responded (${completed}/${total})`);
809
+ }
810
+ )
811
+ : await this.enhancedProviderManager.duckCouncilWithMCP(prompt, { model });
737
812
 
738
813
  const header = '🦆 Duck Council in Session 🦆\n=============================';
739
814
  const formattedResponse = responses
@@ -839,6 +914,9 @@ export class RubberDuckServer {
839
914
  }
840
915
 
841
916
  async stop() {
917
+ // Cleanup task manager (cancel active tasks, clear timers)
918
+ this.taskManager.shutdown();
919
+
842
920
  // Cleanup usage service (flush pending writes)
843
921
  this.usageService.shutdown();
844
922
 
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Progress notification service for MCP tools.
3
+ *
4
+ * Provides a lightweight abstraction over MCP's stable `notifications/progress`
5
+ * mechanism. Tool functions accept an optional `ProgressReporter` to emit
6
+ * per-step progress without depending on the full `RequestHandlerExtra` type.
7
+ */
8
+
9
+ export interface ProgressReporter {
10
+ /** Report that step `current` of `total` is done, with an optional status message. */
11
+ report(current: number, total: number, message?: string): Promise<void>;
12
+ /** Whether the client actually requested progress (sent a progressToken). */
13
+ readonly enabled: boolean;
14
+ }
15
+
16
+ /**
17
+ * Creates a `ProgressReporter` from an MCP tool handler's `extra` parameter.
18
+ *
19
+ * If the client did not include a `progressToken` in `_meta`, the returned
20
+ * reporter is a no-op (`.enabled === false`), so callers can always call
21
+ * `progress.report()` unconditionally.
22
+ *
23
+ * @param progressToken Value of `extra._meta?.progressToken`
24
+ * @param sendNotification Value of `extra.sendNotification` — typed loosely to
25
+ * avoid coupling to SDK's strict discriminated union. At runtime the SDK
26
+ * accepts any valid JSON-RPC notification including `notifications/progress`.
27
+ */
28
+ export function createProgressReporter(
29
+ progressToken: string | number | undefined,
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ sendNotification: (notification: any) => Promise<void>
32
+ ): ProgressReporter {
33
+ if (progressToken === undefined) {
34
+ return {
35
+ enabled: false,
36
+ report: async () => {},
37
+ };
38
+ }
39
+
40
+ return {
41
+ enabled: true,
42
+ report: async (current: number, total: number, message?: string) => {
43
+ try {
44
+ await sendNotification({
45
+ method: 'notifications/progress',
46
+ params: {
47
+ progressToken,
48
+ progress: current,
49
+ total,
50
+ ...(message !== undefined ? { message } : {}),
51
+ },
52
+ });
53
+ } catch {
54
+ // Swallow notification errors (e.g., client disconnected).
55
+ // Progress is best-effort — tool execution should continue regardless.
56
+ }
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Task lifecycle adapter wrapping the MCP SDK's experimental Tasks API.
3
+ *
4
+ * This module isolates all direct usage of `@modelcontextprotocol/sdk/experimental`
5
+ * behind a single adapter class so that future breaking changes in the experimental
6
+ * API only require updates here.
7
+ */
8
+
9
+ import {
10
+ InMemoryTaskStore,
11
+ InMemoryTaskMessageQueue,
12
+ } from '@modelcontextprotocol/sdk/experimental';
13
+ import type {
14
+ TaskStore,
15
+ TaskMessageQueue,
16
+ } from '@modelcontextprotocol/sdk/experimental';
17
+ import type { CallToolResult, Result } from '@modelcontextprotocol/sdk/types.js';
18
+ import { logger } from '../utils/logger.js';
19
+
20
+ export interface TaskManagerConfig {
21
+ /** Time-to-live for completed task results (milliseconds). */
22
+ defaultTtl: number;
23
+ /** Suggested interval between client polls (milliseconds). */
24
+ pollInterval: number;
25
+ /** Maximum messages per task queue (prevents unbounded growth). */
26
+ maxQueueSize: number;
27
+ /** Interval for cleanup/monitoring sweep (milliseconds). */
28
+ cleanupInterval: number;
29
+ }
30
+
31
+ const DEFAULT_CONFIG: TaskManagerConfig = {
32
+ defaultTtl: 300_000, // 5 minutes
33
+ pollInterval: 2_000, // 2 seconds
34
+ maxQueueSize: 100,
35
+ cleanupInterval: 60_000, // 1 minute
36
+ };
37
+
38
+ /**
39
+ * Manages MCP task lifecycle: creation, background execution, cancellation, and cleanup.
40
+ *
41
+ * - Provides `InMemoryTaskStore` and `InMemoryTaskMessageQueue` instances for
42
+ * `McpServer`'s `ProtocolOptions`.
43
+ * - Tracks active background work via `AbortController` per task for cancellation.
44
+ * - Handles graceful shutdown (cancels active tasks, clears timers).
45
+ */
46
+ export class TaskManager {
47
+ readonly taskStore: TaskStore;
48
+ readonly taskMessageQueue: TaskMessageQueue;
49
+ readonly config: TaskManagerConfig;
50
+
51
+ /** Maps taskId → AbortController for active background work. */
52
+ private activeControllers: Map<string, AbortController> = new Map();
53
+ private cleanupTimer?: ReturnType<typeof setInterval>;
54
+
55
+ constructor(config?: Partial<TaskManagerConfig>) {
56
+ this.config = { ...DEFAULT_CONFIG, ...config };
57
+ this.taskStore = new InMemoryTaskStore();
58
+ this.taskMessageQueue = new InMemoryTaskMessageQueue();
59
+ this.startCleanup();
60
+ }
61
+
62
+ /**
63
+ * Start background work for a task.
64
+ *
65
+ * This is fire-and-forget: the returned promise resolves immediately.
66
+ * The `work` function runs asynchronously; its result is stored in the
67
+ * task store on completion. On error the task is marked `failed`.
68
+ * On abort (cancellation) the task is marked `cancelled`.
69
+ *
70
+ * @param taskId ID of the task (from `taskStore.createTask`)
71
+ * @param work Async function receiving an `AbortSignal` and returning a `CallToolResult`
72
+ */
73
+ startBackground(
74
+ taskId: string,
75
+ work: (signal: AbortSignal) => Promise<CallToolResult>
76
+ ): void {
77
+ const controller = new AbortController();
78
+ this.activeControllers.set(taskId, controller);
79
+
80
+ void (async () => {
81
+ try {
82
+ await this.taskStore.updateTaskStatus(taskId, 'working');
83
+ const result = await work(controller.signal);
84
+
85
+ if (!controller.signal.aborted) {
86
+ await this.taskStore.storeTaskResult(taskId, 'completed', result as Result);
87
+ } else {
88
+ // Work completed but cancellation was requested mid-execution.
89
+ // Mark as cancelled so the task doesn't stay stuck in 'working'.
90
+ try {
91
+ await this.taskStore.updateTaskStatus(taskId, 'cancelled', 'Task was cancelled');
92
+ } catch {
93
+ // Task may already be in a terminal state
94
+ }
95
+ }
96
+ } catch (error) {
97
+ if (controller.signal.aborted) {
98
+ try {
99
+ await this.taskStore.updateTaskStatus(taskId, 'cancelled', 'Task was cancelled');
100
+ } catch {
101
+ // Task may already be in a terminal state
102
+ }
103
+ } else {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ logger.error(`Task ${taskId} failed:`, message);
106
+ try {
107
+ await this.taskStore.storeTaskResult(taskId, 'failed', {
108
+ content: [{ type: 'text', text: `Error: ${message}` }],
109
+ isError: true,
110
+ } as Result);
111
+ } catch {
112
+ // Task store may have already cleaned up (TTL)
113
+ }
114
+ }
115
+ } finally {
116
+ this.activeControllers.delete(taskId);
117
+ }
118
+ })();
119
+ }
120
+
121
+ /** Cancel a running task by aborting its AbortController. */
122
+ cancel(taskId: string): boolean {
123
+ const controller = this.activeControllers.get(taskId);
124
+ if (controller) {
125
+ controller.abort();
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+
131
+ /** Number of currently active background tasks. */
132
+ get activeCount(): number {
133
+ return this.activeControllers.size;
134
+ }
135
+
136
+ private startCleanup(): void {
137
+ this.cleanupTimer = setInterval(() => {
138
+ logger.debug(`Active background tasks: ${this.activeControllers.size}`);
139
+ }, this.config.cleanupInterval);
140
+ // Allow the process to exit even if the timer is still running
141
+ if (this.cleanupTimer && typeof this.cleanupTimer === 'object' && 'unref' in this.cleanupTimer) {
142
+ this.cleanupTimer.unref();
143
+ }
144
+ }
145
+
146
+ /** Graceful shutdown: cancel all active tasks and clear timers. */
147
+ shutdown(): void {
148
+ if (this.cleanupTimer) {
149
+ clearInterval(this.cleanupTimer);
150
+ this.cleanupTimer = undefined;
151
+ }
152
+
153
+ for (const [taskId, controller] of this.activeControllers) {
154
+ logger.info(`Cancelling active task ${taskId} during shutdown`);
155
+ controller.abort();
156
+ }
157
+ this.activeControllers.clear();
158
+
159
+ // Clear InMemoryTaskStore internal TTL timers
160
+ (this.taskStore as InMemoryTaskStore).cleanup();
161
+ }
162
+ }
@@ -1,10 +1,12 @@
1
1
  import { ProviderManager } from '../providers/manager.js';
2
2
  import { duckArt } from '../utils/ascii-art.js';
3
3
  import { logger } from '../utils/logger.js';
4
+ import type { ProgressReporter } from '../services/progress.js';
4
5
 
5
6
  export async function compareDucksTool(
6
7
  providerManager: ProviderManager,
7
- args: Record<string, unknown>
8
+ args: Record<string, unknown>,
9
+ progress?: ProgressReporter
8
10
  ) {
9
11
  const { prompt, providers, model } = args as {
10
12
  prompt?: string;
@@ -16,8 +18,17 @@ export async function compareDucksTool(
16
18
  throw new Error('Prompt is required');
17
19
  }
18
20
 
19
- // Get responses from multiple ducks
20
- const responses = await providerManager.compareDucks(prompt, providers, { model });
21
+ // Get responses from multiple ducks, reporting progress as each completes
22
+ const responses = progress
23
+ ? await providerManager.compareDucksWithProgress(
24
+ prompt,
25
+ providers,
26
+ { model },
27
+ (providerName, completed, total) => {
28
+ void progress.report(completed, total, `${providerName} responded (${completed}/${total})`);
29
+ }
30
+ )
31
+ : await providerManager.compareDucks(prompt, providers, { model });
21
32
 
22
33
  // Build comparison response
23
34
  let response = `${duckArt.panel}\n`;