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.
- package/CHANGELOG.md +7 -0
- package/README.md +8 -0
- package/dist/providers/enhanced-manager.d.ts +7 -0
- package/dist/providers/enhanced-manager.d.ts.map +1 -1
- package/dist/providers/enhanced-manager.js +36 -0
- package/dist/providers/enhanced-manager.js.map +1 -1
- package/dist/providers/manager.d.ts +1 -0
- package/dist/providers/manager.d.ts.map +1 -1
- package/dist/providers/manager.js +33 -0
- package/dist/providers/manager.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +93 -33
- package/dist/server.js.map +1 -1
- package/dist/services/progress.d.ts +27 -0
- package/dist/services/progress.d.ts.map +1 -0
- package/dist/services/progress.js +50 -0
- package/dist/services/progress.js.map +1 -0
- package/dist/services/task-manager.d.ts +56 -0
- package/dist/services/task-manager.d.ts.map +1 -0
- package/dist/services/task-manager.js +134 -0
- package/dist/services/task-manager.js.map +1 -0
- package/dist/tools/compare-ducks.d.ts +2 -1
- package/dist/tools/compare-ducks.d.ts.map +1 -1
- package/dist/tools/compare-ducks.js +7 -3
- package/dist/tools/compare-ducks.js.map +1 -1
- package/dist/tools/duck-council.d.ts +2 -1
- package/dist/tools/duck-council.d.ts.map +1 -1
- package/dist/tools/duck-council.js +7 -3
- package/dist/tools/duck-council.js.map +1 -1
- package/dist/tools/duck-debate.d.ts +2 -1
- package/dist/tools/duck-debate.d.ts.map +1 -1
- package/dist/tools/duck-debate.js +19 -1
- package/dist/tools/duck-debate.js.map +1 -1
- package/dist/tools/duck-iterate.d.ts +2 -1
- package/dist/tools/duck-iterate.d.ts.map +1 -1
- package/dist/tools/duck-iterate.js +13 -1
- package/dist/tools/duck-iterate.js.map +1 -1
- package/dist/tools/duck-vote.d.ts +2 -1
- package/dist/tools/duck-vote.d.ts.map +1 -1
- package/dist/tools/duck-vote.js +7 -3
- package/dist/tools/duck-vote.js.map +1 -1
- package/package.json +1 -1
- package/src/providers/enhanced-manager.ts +49 -0
- package/src/providers/manager.ts +45 -0
- package/src/server.ts +110 -32
- package/src/services/progress.ts +59 -0
- package/src/services/task-manager.ts +162 -0
- package/src/tools/compare-ducks.ts +14 -3
- package/src/tools/duck-council.ts +15 -4
- package/src/tools/duck-debate.ts +31 -1
- package/src/tools/duck-iterate.ts +20 -1
- package/src/tools/duck-vote.ts +14 -3
- package/tests/duck-debate.test.ts +80 -0
- package/tests/duck-iterate.test.ts +81 -0
- package/tests/duck-vote.test.ts +70 -0
- package/tests/providers.test.ts +121 -0
- package/tests/services/progress.test.ts +137 -0
- package/tests/services/task-manager.test.ts +344 -0
- package/tests/tools/compare-ducks.test.ts +19 -0
- 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;
|
|
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"}
|
package/dist/tools/duck-vote.js
CHANGED
|
@@ -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 =
|
|
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":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE5D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"duck-vote.js","sourceRoot":"","sources":["../../src/tools/duck-vote.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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>
|
package/src/providers/manager.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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 =
|
|
692
|
-
|
|
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 =
|
|
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 =
|
|
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`;
|