mcp-rubber-duck 1.9.5 → 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/.eslintrc.json +3 -1
- package/CHANGELOG.md +19 -0
- package/README.md +62 -10
- package/assets/ext-apps-compare.png +0 -0
- package/assets/ext-apps-debate.png +0 -0
- package/assets/ext-apps-usage-stats.png +0 -0
- package/assets/ext-apps-vote.png +0 -0
- package/audit-ci.json +2 -1
- 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 +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +154 -36
- 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 +26 -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 +43 -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 +30 -3
- package/dist/tools/duck-vote.js.map +1 -1
- package/dist/tools/get-usage-stats.d.ts.map +1 -1
- package/dist/tools/get-usage-stats.js +13 -0
- package/dist/tools/get-usage-stats.js.map +1 -1
- package/dist/ui/compare-ducks/mcp-app.html +187 -0
- package/dist/ui/duck-debate/mcp-app.html +182 -0
- package/dist/ui/duck-vote/mcp-app.html +168 -0
- package/dist/ui/usage-stats/mcp-app.html +192 -0
- package/jest.config.js +1 -0
- package/package.json +7 -3
- package/src/providers/enhanced-manager.ts +49 -0
- package/src/providers/manager.ts +45 -0
- package/src/server.ts +187 -34
- package/src/services/progress.ts +59 -0
- package/src/services/task-manager.ts +162 -0
- package/src/tools/compare-ducks.ts +34 -3
- package/src/tools/duck-council.ts +15 -4
- package/src/tools/duck-debate.ts +58 -1
- package/src/tools/duck-iterate.ts +20 -1
- package/src/tools/duck-vote.ts +38 -3
- package/src/tools/get-usage-stats.ts +14 -0
- package/src/ui/compare-ducks/app.ts +88 -0
- package/src/ui/compare-ducks/mcp-app.html +102 -0
- package/src/ui/duck-debate/app.ts +111 -0
- package/src/ui/duck-debate/mcp-app.html +97 -0
- package/src/ui/duck-vote/app.ts +128 -0
- package/src/ui/duck-vote/mcp-app.html +83 -0
- package/src/ui/usage-stats/app.ts +156 -0
- package/src/ui/usage-stats/mcp-app.html +107 -0
- package/tests/duck-debate.test.ts +83 -1
- package/tests/duck-iterate.test.ts +81 -0
- package/tests/duck-vote.test.ts +73 -1
- 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-ui.test.ts +135 -0
- package/tests/tools/compare-ducks.test.ts +22 -1
- package/tests/tools/duck-council.test.ts +19 -0
- package/tests/tools/duck-debate-ui.test.ts +234 -0
- package/tests/tools/duck-vote-ui.test.ts +172 -0
- package/tests/tools/get-usage-stats.test.ts +3 -1
- package/tests/tools/usage-stats-ui.test.ts +130 -0
- package/tests/ui-build.test.ts +53 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +19 -0
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-rubber-duck",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"mcp-rubber-duck": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
11
|
+
"build": "tsc && npm run build:ui",
|
|
12
|
+
"build:ui": "VITE_UI_ENTRY=compare-ducks vite build && VITE_UI_ENTRY=duck-vote vite build && VITE_UI_ENTRY=duck-debate vite build && VITE_UI_ENTRY=usage-stats vite build",
|
|
12
13
|
"dev": "tsx watch src/index.ts",
|
|
13
14
|
"start": "node dist/index.js",
|
|
14
15
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"access": "public"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
|
45
47
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
46
48
|
"@semantic-release/npm": "^13.1.3",
|
|
47
49
|
"ajv": "^8.17.1",
|
|
@@ -65,7 +67,9 @@
|
|
|
65
67
|
"semantic-release": "^25.0.2",
|
|
66
68
|
"ts-jest": "^29.0.0",
|
|
67
69
|
"tsx": "^4.0.0",
|
|
68
|
-
"typescript": "^5.0.0"
|
|
70
|
+
"typescript": "^5.0.0",
|
|
71
|
+
"vite": "^7.3.1",
|
|
72
|
+
"vite-plugin-singlefile": "^2.3.0"
|
|
69
73
|
},
|
|
70
74
|
"overrides": {
|
|
71
75
|
"js-yaml": "^4.1.1",
|
|
@@ -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
|
@@ -2,6 +2,14 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import {
|
|
9
|
+
registerAppTool,
|
|
10
|
+
registerAppResource,
|
|
11
|
+
RESOURCE_MIME_TYPE,
|
|
12
|
+
} from '@modelcontextprotocol/ext-apps/server';
|
|
5
13
|
|
|
6
14
|
import { ConfigManager } from './config/config.js';
|
|
7
15
|
import { ProviderManager } from './providers/manager.js';
|
|
@@ -16,6 +24,8 @@ import { DuckResponse } from './config/types.js';
|
|
|
16
24
|
import { ApprovalService } from './services/approval.js';
|
|
17
25
|
import { FunctionBridge } from './services/function-bridge.js';
|
|
18
26
|
import { GuardrailsService } from './guardrails/service.js';
|
|
27
|
+
import { TaskManager } from './services/task-manager.js';
|
|
28
|
+
import { createProgressReporter } from './services/progress.js';
|
|
19
29
|
import { logger } from './utils/logger.js';
|
|
20
30
|
import { duckArt, getRandomDuckMessage } from './utils/ascii-art.js';
|
|
21
31
|
|
|
@@ -61,13 +71,32 @@ export class RubberDuckServer {
|
|
|
61
71
|
private functionBridge?: FunctionBridge;
|
|
62
72
|
private mcpEnabled: boolean = false;
|
|
63
73
|
|
|
74
|
+
// Task management
|
|
75
|
+
private taskManager: TaskManager;
|
|
76
|
+
|
|
64
77
|
constructor() {
|
|
78
|
+
this.taskManager = new TaskManager();
|
|
79
|
+
|
|
65
80
|
this.server = new McpServer(
|
|
66
81
|
{
|
|
67
82
|
name: 'mcp-rubber-duck',
|
|
68
83
|
version: '1.0.0',
|
|
69
84
|
},
|
|
70
|
-
{
|
|
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
|
+
}
|
|
71
100
|
);
|
|
72
101
|
|
|
73
102
|
// Initialize managers
|
|
@@ -94,6 +123,7 @@ export class RubberDuckServer {
|
|
|
94
123
|
|
|
95
124
|
this.registerTools();
|
|
96
125
|
this.registerPrompts();
|
|
126
|
+
this.registerUIResources();
|
|
97
127
|
|
|
98
128
|
// Handle errors
|
|
99
129
|
this.server.server.onerror = (error) => {
|
|
@@ -288,7 +318,8 @@ export class RubberDuckServer {
|
|
|
288
318
|
);
|
|
289
319
|
|
|
290
320
|
// compare_ducks
|
|
291
|
-
|
|
321
|
+
registerAppTool(
|
|
322
|
+
this.server,
|
|
292
323
|
'compare_ducks',
|
|
293
324
|
{
|
|
294
325
|
description: 'Ask the same question to multiple ducks simultaneously',
|
|
@@ -301,13 +332,15 @@ export class RubberDuckServer {
|
|
|
301
332
|
readOnlyHint: true,
|
|
302
333
|
openWorldHint: true,
|
|
303
334
|
},
|
|
335
|
+
_meta: { ui: { resourceUri: 'ui://rubber-duck/compare-ducks' } },
|
|
304
336
|
},
|
|
305
|
-
async (args) => {
|
|
337
|
+
async (args, extra) => {
|
|
306
338
|
try {
|
|
339
|
+
const progress = createProgressReporter(extra._meta?.progressToken, extra.sendNotification);
|
|
307
340
|
if (this.mcpEnabled && this.enhancedProviderManager) {
|
|
308
|
-
return this.toolResult(await this.handleCompareDucksWithMCP(args as Record<string, unknown
|
|
341
|
+
return this.toolResult(await this.handleCompareDucksWithMCP(args as Record<string, unknown>, progress));
|
|
309
342
|
}
|
|
310
|
-
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));
|
|
311
344
|
} catch (error) {
|
|
312
345
|
return this.toolErrorResult(error);
|
|
313
346
|
}
|
|
@@ -328,12 +361,13 @@ export class RubberDuckServer {
|
|
|
328
361
|
openWorldHint: true,
|
|
329
362
|
},
|
|
330
363
|
},
|
|
331
|
-
async (args) => {
|
|
364
|
+
async (args, extra) => {
|
|
332
365
|
try {
|
|
366
|
+
const progress = createProgressReporter(extra._meta?.progressToken, extra.sendNotification);
|
|
333
367
|
if (this.mcpEnabled && this.enhancedProviderManager) {
|
|
334
|
-
return this.toolResult(await this.handleDuckCouncilWithMCP(args as Record<string, unknown
|
|
368
|
+
return this.toolResult(await this.handleDuckCouncilWithMCP(args as Record<string, unknown>, progress));
|
|
335
369
|
}
|
|
336
|
-
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));
|
|
337
371
|
} catch (error) {
|
|
338
372
|
return this.toolErrorResult(error);
|
|
339
373
|
}
|
|
@@ -341,7 +375,8 @@ export class RubberDuckServer {
|
|
|
341
375
|
);
|
|
342
376
|
|
|
343
377
|
// duck_vote
|
|
344
|
-
|
|
378
|
+
registerAppTool(
|
|
379
|
+
this.server,
|
|
345
380
|
'duck_vote',
|
|
346
381
|
{
|
|
347
382
|
description: 'Have multiple ducks vote on options with reasoning. Returns vote tally, confidence scores, and consensus level.',
|
|
@@ -355,10 +390,12 @@ export class RubberDuckServer {
|
|
|
355
390
|
readOnlyHint: true,
|
|
356
391
|
openWorldHint: true,
|
|
357
392
|
},
|
|
393
|
+
_meta: { ui: { resourceUri: 'ui://rubber-duck/duck-vote' } },
|
|
358
394
|
},
|
|
359
|
-
async (args) => {
|
|
395
|
+
async (args, extra) => {
|
|
360
396
|
try {
|
|
361
|
-
|
|
397
|
+
const progress = createProgressReporter(extra._meta?.progressToken, extra.sendNotification);
|
|
398
|
+
return this.toolResult(await duckVoteTool(this.providerManager, args as Record<string, unknown>, progress));
|
|
362
399
|
} catch (error) {
|
|
363
400
|
return this.toolErrorResult(error);
|
|
364
401
|
}
|
|
@@ -395,8 +432,8 @@ export class RubberDuckServer {
|
|
|
395
432
|
}
|
|
396
433
|
);
|
|
397
434
|
|
|
398
|
-
// duck_iterate
|
|
399
|
-
this.server.
|
|
435
|
+
// duck_iterate (task-based: supports async execution for long-running iterations)
|
|
436
|
+
this.server.experimental.tasks.registerToolTask(
|
|
400
437
|
'duck_iterate',
|
|
401
438
|
{
|
|
402
439
|
description: 'Iteratively refine a response between two ducks. One generates, the other critiques/improves, alternating for multiple rounds.',
|
|
@@ -410,18 +447,36 @@ export class RubberDuckServer {
|
|
|
410
447
|
readOnlyHint: true,
|
|
411
448
|
openWorldHint: true,
|
|
412
449
|
},
|
|
450
|
+
execution: {
|
|
451
|
+
taskSupport: 'optional',
|
|
452
|
+
},
|
|
413
453
|
},
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
+
},
|
|
420
475
|
}
|
|
421
476
|
);
|
|
422
477
|
|
|
423
|
-
// duck_debate
|
|
424
|
-
this.server.
|
|
478
|
+
// duck_debate (task-based: supports async execution for multi-round debates)
|
|
479
|
+
this.server.experimental.tasks.registerToolTask(
|
|
425
480
|
'duck_debate',
|
|
426
481
|
{
|
|
427
482
|
description: 'Structured multi-round debate between ducks. Supports oxford (pro/con), socratic (questioning), and adversarial (attack/defend) formats.',
|
|
@@ -436,18 +491,38 @@ export class RubberDuckServer {
|
|
|
436
491
|
readOnlyHint: true,
|
|
437
492
|
openWorldHint: true,
|
|
438
493
|
},
|
|
494
|
+
_meta: { ui: { resourceUri: 'ui://rubber-duck/duck-debate' } },
|
|
495
|
+
execution: {
|
|
496
|
+
taskSupport: 'optional',
|
|
497
|
+
},
|
|
439
498
|
},
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
},
|
|
446
520
|
}
|
|
447
521
|
);
|
|
448
522
|
|
|
449
523
|
// get_usage_stats
|
|
450
|
-
|
|
524
|
+
registerAppTool(
|
|
525
|
+
this.server,
|
|
451
526
|
'get_usage_stats',
|
|
452
527
|
{
|
|
453
528
|
description: 'Get usage statistics for a time period. Shows token counts and costs (when pricing configured).',
|
|
@@ -458,6 +533,7 @@ export class RubberDuckServer {
|
|
|
458
533
|
readOnlyHint: true,
|
|
459
534
|
openWorldHint: false,
|
|
460
535
|
},
|
|
536
|
+
_meta: { ui: { resourceUri: 'ui://rubber-duck/usage-stats' } },
|
|
461
537
|
},
|
|
462
538
|
(args) => {
|
|
463
539
|
try {
|
|
@@ -581,6 +657,44 @@ export class RubberDuckServer {
|
|
|
581
657
|
}
|
|
582
658
|
}
|
|
583
659
|
|
|
660
|
+
private registerUIResources() {
|
|
661
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
662
|
+
const uiDir = join(currentDir, '..', 'dist', 'ui');
|
|
663
|
+
|
|
664
|
+
const uiApps = [
|
|
665
|
+
{ name: 'Compare Ducks', uri: 'ui://rubber-duck/compare-ducks', file: 'compare-ducks/mcp-app.html' },
|
|
666
|
+
{ name: 'Duck Vote', uri: 'ui://rubber-duck/duck-vote', file: 'duck-vote/mcp-app.html' },
|
|
667
|
+
{ name: 'Duck Debate', uri: 'ui://rubber-duck/duck-debate', file: 'duck-debate/mcp-app.html' },
|
|
668
|
+
{ name: 'Usage Stats', uri: 'ui://rubber-duck/usage-stats', file: 'usage-stats/mcp-app.html' },
|
|
669
|
+
];
|
|
670
|
+
|
|
671
|
+
for (const app of uiApps) {
|
|
672
|
+
registerAppResource(
|
|
673
|
+
this.server,
|
|
674
|
+
app.name,
|
|
675
|
+
app.uri,
|
|
676
|
+
{ description: `Interactive UI for ${app.name}` },
|
|
677
|
+
() => {
|
|
678
|
+
let html: string;
|
|
679
|
+
try {
|
|
680
|
+
html = readFileSync(join(uiDir, app.file), 'utf-8');
|
|
681
|
+
} catch {
|
|
682
|
+
html = `<html><body><p>UI not built. Run npm run build:ui</p></body></html>`;
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
contents: [
|
|
686
|
+
{
|
|
687
|
+
uri: app.uri,
|
|
688
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
689
|
+
text: html,
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
584
698
|
// MCP-enhanced tool handlers
|
|
585
699
|
private async handleAskDuckWithMCP(args: Record<string, unknown>) {
|
|
586
700
|
if (!this.enhancedProviderManager || !this.cache) {
|
|
@@ -622,7 +736,7 @@ export class RubberDuckServer {
|
|
|
622
736
|
};
|
|
623
737
|
}
|
|
624
738
|
|
|
625
|
-
private async handleCompareDucksWithMCP(args: Record<string, unknown
|
|
739
|
+
private async handleCompareDucksWithMCP(args: Record<string, unknown>, progress?: import('./services/progress.js').ProgressReporter) {
|
|
626
740
|
if (!this.enhancedProviderManager) {
|
|
627
741
|
throw new Error('Enhanced provider manager not available');
|
|
628
742
|
}
|
|
@@ -633,32 +747,68 @@ export class RubberDuckServer {
|
|
|
633
747
|
model?: string;
|
|
634
748
|
};
|
|
635
749
|
|
|
636
|
-
const responses =
|
|
637
|
-
|
|
638
|
-
|
|
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 });
|
|
639
760
|
|
|
640
761
|
const formattedResponse = responses
|
|
641
762
|
.map((response) => this.formatEnhancedDuckResponse(response))
|
|
642
763
|
.join('\n\n═══════════════════════════════════════\n\n');
|
|
643
764
|
|
|
765
|
+
// Build structured data for UI consumption (same shape as compareDucksTool)
|
|
766
|
+
const structuredData = responses.map(r => ({
|
|
767
|
+
provider: r.provider,
|
|
768
|
+
nickname: r.nickname,
|
|
769
|
+
model: r.model,
|
|
770
|
+
content: r.content,
|
|
771
|
+
latency: r.latency,
|
|
772
|
+
tokens: r.usage ? {
|
|
773
|
+
prompt: r.usage.prompt_tokens,
|
|
774
|
+
completion: r.usage.completion_tokens,
|
|
775
|
+
total: r.usage.total_tokens,
|
|
776
|
+
} : null,
|
|
777
|
+
cached: r.cached,
|
|
778
|
+
error: r.content.startsWith('Error:') ? r.content : undefined,
|
|
779
|
+
}));
|
|
780
|
+
|
|
644
781
|
return {
|
|
645
782
|
content: [
|
|
646
783
|
{
|
|
647
784
|
type: 'text' as const,
|
|
648
785
|
text: formattedResponse,
|
|
649
786
|
},
|
|
787
|
+
{
|
|
788
|
+
type: 'text' as const,
|
|
789
|
+
text: JSON.stringify(structuredData),
|
|
790
|
+
},
|
|
650
791
|
],
|
|
651
792
|
};
|
|
652
793
|
}
|
|
653
794
|
|
|
654
|
-
private async handleDuckCouncilWithMCP(args: Record<string, unknown
|
|
795
|
+
private async handleDuckCouncilWithMCP(args: Record<string, unknown>, progress?: import('./services/progress.js').ProgressReporter) {
|
|
655
796
|
if (!this.enhancedProviderManager) {
|
|
656
797
|
throw new Error('Enhanced provider manager not available');
|
|
657
798
|
}
|
|
658
799
|
|
|
659
800
|
const { prompt, model } = args as { prompt: string; model?: string };
|
|
660
801
|
|
|
661
|
-
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 });
|
|
662
812
|
|
|
663
813
|
const header = '🦆 Duck Council in Session 🦆\n=============================';
|
|
664
814
|
const formattedResponse = responses
|
|
@@ -764,6 +914,9 @@ export class RubberDuckServer {
|
|
|
764
914
|
}
|
|
765
915
|
|
|
766
916
|
async stop() {
|
|
917
|
+
// Cleanup task manager (cancel active tasks, clear timers)
|
|
918
|
+
this.taskManager.shutdown();
|
|
919
|
+
|
|
767
920
|
// Cleanup usage service (flush pending writes)
|
|
768
921
|
this.usageService.shutdown();
|
|
769
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
|
+
}
|