mcp-rubber-duck 1.9.4 → 1.10.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 +54 -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 +3 -1
- package/dist/server.d.ts +5 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +414 -498
- package/dist/server.js.map +1 -1
- package/dist/tools/compare-ducks.d.ts.map +1 -1
- package/dist/tools/compare-ducks.js +19 -0
- package/dist/tools/compare-ducks.js.map +1 -1
- package/dist/tools/duck-debate.d.ts.map +1 -1
- package/dist/tools/duck-debate.js +24 -0
- package/dist/tools/duck-debate.js.map +1 -1
- package/dist/tools/duck-vote.d.ts.map +1 -1
- package/dist/tools/duck-vote.js +23 -0
- 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/server.ts +491 -523
- package/src/tools/compare-ducks.ts +20 -0
- package/src/tools/duck-debate.ts +27 -0
- package/src/tools/duck-vote.ts +24 -0
- 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 +3 -1
- package/tests/duck-vote.test.ts +3 -1
- package/tests/tool-annotations.test.ts +208 -41
- package/tests/tools/compare-ducks-ui.test.ts +135 -0
- package/tests/tools/compare-ducks.test.ts +3 -1
- 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/src/server.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
3
8
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Tool,
|
|
9
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
registerAppTool,
|
|
10
|
+
registerAppResource,
|
|
11
|
+
RESOURCE_MIME_TYPE,
|
|
12
|
+
} from '@modelcontextprotocol/ext-apps/server';
|
|
10
13
|
|
|
11
14
|
import { ConfigManager } from './config/config.js';
|
|
12
15
|
import { ProviderManager } from './providers/manager.js';
|
|
@@ -46,10 +49,10 @@ import { mcpStatusTool } from './tools/mcp-status.js';
|
|
|
46
49
|
import { getUsageStatsTool } from './tools/get-usage-stats.js';
|
|
47
50
|
|
|
48
51
|
// Import prompts
|
|
49
|
-
import {
|
|
52
|
+
import { PROMPTS } from './prompts/index.js';
|
|
50
53
|
|
|
51
54
|
export class RubberDuckServer {
|
|
52
|
-
private server:
|
|
55
|
+
private server: McpServer;
|
|
53
56
|
private configManager: ConfigManager;
|
|
54
57
|
private pricingService: PricingService;
|
|
55
58
|
private usageService: UsageService;
|
|
@@ -67,17 +70,12 @@ export class RubberDuckServer {
|
|
|
67
70
|
private mcpEnabled: boolean = false;
|
|
68
71
|
|
|
69
72
|
constructor() {
|
|
70
|
-
this.server = new
|
|
73
|
+
this.server = new McpServer(
|
|
71
74
|
{
|
|
72
75
|
name: 'mcp-rubber-duck',
|
|
73
76
|
version: '1.0.0',
|
|
74
77
|
},
|
|
75
|
-
{
|
|
76
|
-
capabilities: {
|
|
77
|
-
tools: {},
|
|
78
|
-
prompts: {},
|
|
79
|
-
},
|
|
80
|
-
}
|
|
78
|
+
{}
|
|
81
79
|
);
|
|
82
80
|
|
|
83
81
|
// Initialize managers
|
|
@@ -102,7 +100,14 @@ export class RubberDuckServer {
|
|
|
102
100
|
// Initialize MCP bridge if enabled
|
|
103
101
|
this.initializeMCPBridge();
|
|
104
102
|
|
|
105
|
-
this.
|
|
103
|
+
this.registerTools();
|
|
104
|
+
this.registerPrompts();
|
|
105
|
+
this.registerUIResources();
|
|
106
|
+
|
|
107
|
+
// Handle errors
|
|
108
|
+
this.server.server.onerror = (error) => {
|
|
109
|
+
logger.error('Server error:', error);
|
|
110
|
+
};
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
private initializeMCPBridge(): void {
|
|
@@ -151,130 +156,484 @@ export class RubberDuckServer {
|
|
|
151
156
|
}
|
|
152
157
|
}
|
|
153
158
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
// Tool functions return `{ type: 'text' }` where TS infers `string`, not the literal `"text"`.
|
|
160
|
+
// This helper narrows the type to satisfy McpServer's CallToolResult expectation.
|
|
161
|
+
private toolResult(result: { content: { type: string; text: string }[]; isError?: boolean }): CallToolResult {
|
|
162
|
+
return result as CallToolResult;
|
|
163
|
+
}
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
private toolErrorResult(error: unknown): CallToolResult {
|
|
166
|
+
logger.error('Tool execution error:', error);
|
|
167
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: 'text' as const,
|
|
172
|
+
text: `${getRandomDuckMessage('error')}\n\nError: ${errorMessage}`,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
isError: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
164
178
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
private registerTools() {
|
|
180
|
+
// ask_duck
|
|
181
|
+
this.server.registerTool(
|
|
182
|
+
'ask_duck',
|
|
183
|
+
{
|
|
184
|
+
description: this.mcpEnabled
|
|
185
|
+
? 'Ask a question to a specific LLM provider (duck) with MCP tool access'
|
|
186
|
+
: 'Ask a question to a specific LLM provider (duck)',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
prompt: z.string().describe('The question or prompt to send to the duck'),
|
|
189
|
+
provider: z.string().optional().describe('The provider name (optional, uses default if not specified)'),
|
|
190
|
+
model: z.string().optional().describe('Specific model to use (optional, uses provider default if not specified)'),
|
|
191
|
+
temperature: z.number().min(0).max(2).optional().describe('Temperature for response generation (0-2)'),
|
|
192
|
+
},
|
|
193
|
+
annotations: {
|
|
194
|
+
readOnlyHint: true,
|
|
195
|
+
openWorldHint: true,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
async (args) => {
|
|
199
|
+
try {
|
|
200
|
+
if (this.mcpEnabled && this.enhancedProviderManager) {
|
|
201
|
+
return this.toolResult(await this.handleAskDuckWithMCP(args as Record<string, unknown>));
|
|
202
|
+
}
|
|
203
|
+
return this.toolResult(await askDuckTool(this.providerManager, this.cache, args as Record<string, unknown>));
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return this.toolErrorResult(error);
|
|
206
|
+
}
|
|
174
207
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Handle tool calls
|
|
178
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
179
|
-
const { name, arguments: args } = request.params;
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
switch (name) {
|
|
183
|
-
case 'ask_duck':
|
|
184
|
-
// Use enhanced provider manager if MCP is enabled
|
|
185
|
-
if (this.mcpEnabled && this.enhancedProviderManager) {
|
|
186
|
-
return await this.handleAskDuckWithMCP(args || {});
|
|
187
|
-
}
|
|
188
|
-
return await askDuckTool(this.providerManager, this.cache, args || {});
|
|
208
|
+
);
|
|
189
209
|
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
// chat_with_duck
|
|
211
|
+
this.server.registerTool(
|
|
212
|
+
'chat_with_duck',
|
|
213
|
+
{
|
|
214
|
+
description: 'Have a conversation with a duck, maintaining context across messages',
|
|
215
|
+
inputSchema: {
|
|
216
|
+
conversation_id: z.string().describe('Conversation ID (creates new if not exists)'),
|
|
217
|
+
message: z.string().describe('Your message to the duck'),
|
|
218
|
+
provider: z.string().optional().describe('Provider to use (can switch mid-conversation)'),
|
|
219
|
+
model: z.string().optional().describe('Specific model to use (optional)'),
|
|
220
|
+
},
|
|
221
|
+
annotations: {
|
|
222
|
+
openWorldHint: true,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
async (args) => {
|
|
226
|
+
try {
|
|
227
|
+
return this.toolResult(await chatDuckTool(this.providerManager, this.conversationManager, args as Record<string, unknown>));
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return this.toolErrorResult(error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
);
|
|
192
233
|
|
|
193
|
-
|
|
194
|
-
|
|
234
|
+
// clear_conversations
|
|
235
|
+
this.server.registerTool(
|
|
236
|
+
'clear_conversations',
|
|
237
|
+
{
|
|
238
|
+
description: 'Clear all conversation history and start fresh',
|
|
239
|
+
annotations: {
|
|
240
|
+
destructiveHint: true,
|
|
241
|
+
idempotentHint: true,
|
|
242
|
+
openWorldHint: false,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
() => {
|
|
246
|
+
try {
|
|
247
|
+
return this.toolResult(clearConversationsTool(this.conversationManager, {}));
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return this.toolErrorResult(error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
);
|
|
195
253
|
|
|
196
|
-
|
|
197
|
-
|
|
254
|
+
// list_ducks
|
|
255
|
+
this.server.registerTool(
|
|
256
|
+
'list_ducks',
|
|
257
|
+
{
|
|
258
|
+
description: 'List all available LLM providers (ducks) and their status',
|
|
259
|
+
inputSchema: {
|
|
260
|
+
check_health: z.boolean().default(false).describe('Perform health check on all providers'),
|
|
261
|
+
},
|
|
262
|
+
annotations: {
|
|
263
|
+
readOnlyHint: true,
|
|
264
|
+
openWorldHint: true,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
async (args) => {
|
|
268
|
+
try {
|
|
269
|
+
return this.toolResult(await listDucksTool(this.providerManager, this.healthMonitor, args as Record<string, unknown>));
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return this.toolErrorResult(error);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
);
|
|
198
275
|
|
|
199
|
-
|
|
200
|
-
|
|
276
|
+
// list_models
|
|
277
|
+
this.server.registerTool(
|
|
278
|
+
'list_models',
|
|
279
|
+
{
|
|
280
|
+
description: 'List available models for LLM providers',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
provider: z.string().optional().describe('Provider name (optional, lists all if not specified)'),
|
|
283
|
+
fetch_latest: z.boolean().default(false).describe('Fetch latest models from API vs using cached/configured'),
|
|
284
|
+
},
|
|
285
|
+
annotations: {
|
|
286
|
+
readOnlyHint: true,
|
|
287
|
+
openWorldHint: true,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
async (args) => {
|
|
291
|
+
try {
|
|
292
|
+
return this.toolResult(await listModelsTool(this.providerManager, args as Record<string, unknown>));
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return this.toolErrorResult(error);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
);
|
|
201
298
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
299
|
+
// compare_ducks
|
|
300
|
+
registerAppTool(
|
|
301
|
+
this.server,
|
|
302
|
+
'compare_ducks',
|
|
303
|
+
{
|
|
304
|
+
description: 'Ask the same question to multiple ducks simultaneously',
|
|
305
|
+
inputSchema: {
|
|
306
|
+
prompt: z.string().describe('The question to ask all ducks'),
|
|
307
|
+
providers: z.array(z.string()).optional().describe('List of provider names to query (optional, uses all if not specified)'),
|
|
308
|
+
model: z.string().optional().describe('Specific model to use for all providers (optional)'),
|
|
309
|
+
},
|
|
310
|
+
annotations: {
|
|
311
|
+
readOnlyHint: true,
|
|
312
|
+
openWorldHint: true,
|
|
313
|
+
},
|
|
314
|
+
_meta: { ui: { resourceUri: 'ui://rubber-duck/compare-ducks' } },
|
|
315
|
+
},
|
|
316
|
+
async (args) => {
|
|
317
|
+
try {
|
|
318
|
+
if (this.mcpEnabled && this.enhancedProviderManager) {
|
|
319
|
+
return this.toolResult(await this.handleCompareDucksWithMCP(args as Record<string, unknown>));
|
|
320
|
+
}
|
|
321
|
+
return this.toolResult(await compareDucksTool(this.providerManager, args as Record<string, unknown>));
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return this.toolErrorResult(error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
);
|
|
208
327
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
328
|
+
// duck_council
|
|
329
|
+
this.server.registerTool(
|
|
330
|
+
'duck_council',
|
|
331
|
+
{
|
|
332
|
+
description: 'Get responses from all configured ducks (like a panel discussion)',
|
|
333
|
+
inputSchema: {
|
|
334
|
+
prompt: z.string().describe('The question for the duck council'),
|
|
335
|
+
model: z.string().optional().describe('Specific model to use for all ducks (optional)'),
|
|
336
|
+
},
|
|
337
|
+
annotations: {
|
|
338
|
+
readOnlyHint: true,
|
|
339
|
+
openWorldHint: true,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
async (args) => {
|
|
343
|
+
try {
|
|
344
|
+
if (this.mcpEnabled && this.enhancedProviderManager) {
|
|
345
|
+
return this.toolResult(await this.handleDuckCouncilWithMCP(args as Record<string, unknown>));
|
|
346
|
+
}
|
|
347
|
+
return this.toolResult(await duckCouncilTool(this.providerManager, args as Record<string, unknown>));
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return this.toolErrorResult(error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
);
|
|
215
353
|
|
|
216
|
-
|
|
217
|
-
|
|
354
|
+
// duck_vote
|
|
355
|
+
registerAppTool(
|
|
356
|
+
this.server,
|
|
357
|
+
'duck_vote',
|
|
358
|
+
{
|
|
359
|
+
description: 'Have multiple ducks vote on options with reasoning. Returns vote tally, confidence scores, and consensus level.',
|
|
360
|
+
inputSchema: {
|
|
361
|
+
question: z.string().describe('The question to vote on (e.g., "Best approach for error handling?")'),
|
|
362
|
+
options: z.array(z.string()).min(2).max(10).describe('The options to vote on (2-10 options)'),
|
|
363
|
+
voters: z.array(z.string()).optional().describe('List of provider names to vote (optional, uses all if not specified)'),
|
|
364
|
+
require_reasoning: z.boolean().default(true).describe('Require ducks to explain their vote (default: true)'),
|
|
365
|
+
},
|
|
366
|
+
annotations: {
|
|
367
|
+
readOnlyHint: true,
|
|
368
|
+
openWorldHint: true,
|
|
369
|
+
},
|
|
370
|
+
_meta: { ui: { resourceUri: 'ui://rubber-duck/duck-vote' } },
|
|
371
|
+
},
|
|
372
|
+
async (args) => {
|
|
373
|
+
try {
|
|
374
|
+
return this.toolResult(await duckVoteTool(this.providerManager, args as Record<string, unknown>));
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return this.toolErrorResult(error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
);
|
|
218
380
|
|
|
219
|
-
|
|
220
|
-
|
|
381
|
+
// duck_judge
|
|
382
|
+
this.server.registerTool(
|
|
383
|
+
'duck_judge',
|
|
384
|
+
{
|
|
385
|
+
description: 'Have one duck evaluate and rank other ducks\' responses. Use after duck_council to get a comparative evaluation.',
|
|
386
|
+
inputSchema: {
|
|
387
|
+
responses: z.array(z.object({
|
|
388
|
+
provider: z.string(),
|
|
389
|
+
nickname: z.string(),
|
|
390
|
+
model: z.string().optional(),
|
|
391
|
+
content: z.string(),
|
|
392
|
+
})).min(2).describe('Array of duck responses to evaluate (from duck_council output)'),
|
|
393
|
+
judge: z.string().optional().describe('Provider name of the judge duck (optional, uses first available)'),
|
|
394
|
+
criteria: z.array(z.string()).optional().describe('Evaluation criteria (default: ["accuracy", "completeness", "clarity"])'),
|
|
395
|
+
persona: z.string().optional().describe('Judge persona (e.g., "senior engineer", "security expert")'),
|
|
396
|
+
},
|
|
397
|
+
annotations: {
|
|
398
|
+
readOnlyHint: true,
|
|
399
|
+
openWorldHint: true,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
async (args) => {
|
|
403
|
+
try {
|
|
404
|
+
return this.toolResult(await duckJudgeTool(this.providerManager, args as Record<string, unknown>));
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return this.toolErrorResult(error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
);
|
|
221
410
|
|
|
222
|
-
|
|
223
|
-
|
|
411
|
+
// duck_iterate
|
|
412
|
+
this.server.registerTool(
|
|
413
|
+
'duck_iterate',
|
|
414
|
+
{
|
|
415
|
+
description: 'Iteratively refine a response between two ducks. One generates, the other critiques/improves, alternating for multiple rounds.',
|
|
416
|
+
inputSchema: {
|
|
417
|
+
prompt: z.string().describe('The initial prompt/task to iterate on'),
|
|
418
|
+
iterations: z.number().min(1).max(10).default(3).describe('Number of iteration rounds (default: 3, max: 10)'),
|
|
419
|
+
providers: z.array(z.string()).min(2).max(2).describe('Exactly 2 provider names for the ping-pong iteration'),
|
|
420
|
+
mode: z.enum(['refine', 'critique-improve']).describe('refine: each duck improves the previous response. critique-improve: alternates between critiquing and improving.'),
|
|
421
|
+
},
|
|
422
|
+
annotations: {
|
|
423
|
+
readOnlyHint: true,
|
|
424
|
+
openWorldHint: true,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
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
|
+
}
|
|
433
|
+
}
|
|
434
|
+
);
|
|
224
435
|
|
|
225
|
-
|
|
226
|
-
|
|
436
|
+
// duck_debate
|
|
437
|
+
registerAppTool(
|
|
438
|
+
this.server,
|
|
439
|
+
'duck_debate',
|
|
440
|
+
{
|
|
441
|
+
description: 'Structured multi-round debate between ducks. Supports oxford (pro/con), socratic (questioning), and adversarial (attack/defend) formats.',
|
|
442
|
+
inputSchema: {
|
|
443
|
+
prompt: z.string().describe('The debate topic or proposition'),
|
|
444
|
+
rounds: z.number().min(1).max(10).default(3).describe('Number of debate rounds (default: 3)'),
|
|
445
|
+
providers: z.array(z.string()).min(2).optional().describe('Provider names to participate (min 2, uses all if not specified)'),
|
|
446
|
+
format: z.enum(['oxford', 'socratic', 'adversarial']).describe('Debate format: oxford (pro/con), socratic (questioning), adversarial (attack/defend)'),
|
|
447
|
+
synthesizer: z.string().optional().describe('Provider to synthesize the debate (optional, uses first provider)'),
|
|
448
|
+
},
|
|
449
|
+
annotations: {
|
|
450
|
+
readOnlyHint: true,
|
|
451
|
+
openWorldHint: true,
|
|
452
|
+
},
|
|
453
|
+
_meta: { ui: { resourceUri: 'ui://rubber-duck/duck-debate' } },
|
|
454
|
+
},
|
|
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
|
+
}
|
|
461
|
+
}
|
|
462
|
+
);
|
|
227
463
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
464
|
+
// get_usage_stats
|
|
465
|
+
registerAppTool(
|
|
466
|
+
this.server,
|
|
467
|
+
'get_usage_stats',
|
|
468
|
+
{
|
|
469
|
+
description: 'Get usage statistics for a time period. Shows token counts and costs (when pricing configured).',
|
|
470
|
+
inputSchema: {
|
|
471
|
+
period: z.enum(['today', '7d', '30d', 'all']).default('today').describe('Time period for stats'),
|
|
472
|
+
},
|
|
473
|
+
annotations: {
|
|
474
|
+
readOnlyHint: true,
|
|
475
|
+
openWorldHint: false,
|
|
476
|
+
},
|
|
477
|
+
_meta: { ui: { resourceUri: 'ui://rubber-duck/usage-stats' } },
|
|
478
|
+
},
|
|
479
|
+
(args) => {
|
|
480
|
+
try {
|
|
481
|
+
return this.toolResult(getUsageStatsTool(this.usageService, args as Record<string, unknown>));
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return this.toolErrorResult(error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
);
|
|
231
487
|
|
|
232
|
-
|
|
233
|
-
|
|
488
|
+
// Conditionally register MCP tools
|
|
489
|
+
if (this.mcpEnabled) {
|
|
490
|
+
// get_pending_approvals
|
|
491
|
+
this.server.registerTool(
|
|
492
|
+
'get_pending_approvals',
|
|
493
|
+
{
|
|
494
|
+
description: 'Get list of pending MCP tool approvals from ducks',
|
|
495
|
+
inputSchema: {
|
|
496
|
+
duck: z.string().optional().describe('Filter by duck name (optional)'),
|
|
497
|
+
},
|
|
498
|
+
annotations: {
|
|
499
|
+
readOnlyHint: true,
|
|
500
|
+
openWorldHint: false,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
(args) => {
|
|
504
|
+
try {
|
|
234
505
|
if (!this.approvalService) {
|
|
235
506
|
throw new Error('MCP bridge not enabled');
|
|
236
507
|
}
|
|
237
|
-
return getPendingApprovalsTool(this.approvalService, args
|
|
508
|
+
return this.toolResult(getPendingApprovalsTool(this.approvalService, args as Record<string, unknown>));
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return this.toolErrorResult(error);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
);
|
|
238
514
|
|
|
239
|
-
|
|
515
|
+
// approve_mcp_request
|
|
516
|
+
this.server.registerTool(
|
|
517
|
+
'approve_mcp_request',
|
|
518
|
+
{
|
|
519
|
+
description: "Approve or deny a duck's MCP tool request",
|
|
520
|
+
inputSchema: {
|
|
521
|
+
approval_id: z.string().describe('The approval request ID'),
|
|
522
|
+
decision: z.enum(['approve', 'deny']).describe('Whether to approve or deny the request'),
|
|
523
|
+
reason: z.string().optional().describe('Reason for denial (optional)'),
|
|
524
|
+
},
|
|
525
|
+
annotations: {
|
|
526
|
+
idempotentHint: true,
|
|
527
|
+
openWorldHint: false,
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
(args) => {
|
|
531
|
+
try {
|
|
240
532
|
if (!this.approvalService) {
|
|
241
533
|
throw new Error('MCP bridge not enabled');
|
|
242
534
|
}
|
|
243
|
-
return approveMCPRequestTool(this.approvalService, args
|
|
535
|
+
return this.toolResult(approveMCPRequestTool(this.approvalService, args as Record<string, unknown>));
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return this.toolErrorResult(error);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
);
|
|
244
541
|
|
|
245
|
-
|
|
542
|
+
// mcp_status
|
|
543
|
+
this.server.registerTool(
|
|
544
|
+
'mcp_status',
|
|
545
|
+
{
|
|
546
|
+
description: 'Get status of MCP bridge, servers, and pending approvals',
|
|
547
|
+
annotations: {
|
|
548
|
+
readOnlyHint: true,
|
|
549
|
+
openWorldHint: true,
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
async () => {
|
|
553
|
+
try {
|
|
246
554
|
if (!this.mcpClientManager || !this.approvalService || !this.functionBridge) {
|
|
247
555
|
throw new Error('MCP bridge not enabled');
|
|
248
556
|
}
|
|
249
|
-
return await mcpStatusTool(
|
|
557
|
+
return this.toolResult(await mcpStatusTool(
|
|
250
558
|
this.mcpClientManager,
|
|
251
559
|
this.approvalService,
|
|
252
560
|
this.functionBridge,
|
|
253
|
-
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
561
|
+
{}
|
|
562
|
+
));
|
|
563
|
+
} catch (error) {
|
|
564
|
+
return this.toolErrorResult(error);
|
|
565
|
+
}
|
|
258
566
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private registerPrompts() {
|
|
572
|
+
for (const [name, prompt] of Object.entries(PROMPTS)) {
|
|
573
|
+
// Convert prompt.arguments array to Zod schema
|
|
574
|
+
const argsSchema: Record<string, z.ZodType> = {};
|
|
575
|
+
for (const arg of prompt.arguments || []) {
|
|
576
|
+
argsSchema[arg.name] = arg.required
|
|
577
|
+
? z.string().describe(arg.description || '')
|
|
578
|
+
: z.string().optional().describe(arg.description || '');
|
|
271
579
|
}
|
|
272
|
-
});
|
|
273
580
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
581
|
+
this.server.registerPrompt(
|
|
582
|
+
name,
|
|
583
|
+
{
|
|
584
|
+
description: prompt.description,
|
|
585
|
+
argsSchema,
|
|
586
|
+
},
|
|
587
|
+
(args) => {
|
|
588
|
+
try {
|
|
589
|
+
const messages = prompt.buildMessages((args || {}) as Record<string, string>);
|
|
590
|
+
return { messages };
|
|
591
|
+
} catch (error: unknown) {
|
|
592
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
593
|
+
logger.error(`Prompt error for ${name}:`, errorMessage);
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private registerUIResources() {
|
|
602
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
603
|
+
const uiDir = join(currentDir, '..', 'dist', 'ui');
|
|
604
|
+
|
|
605
|
+
const uiApps = [
|
|
606
|
+
{ name: 'Compare Ducks', uri: 'ui://rubber-duck/compare-ducks', file: 'compare-ducks/mcp-app.html' },
|
|
607
|
+
{ name: 'Duck Vote', uri: 'ui://rubber-duck/duck-vote', file: 'duck-vote/mcp-app.html' },
|
|
608
|
+
{ name: 'Duck Debate', uri: 'ui://rubber-duck/duck-debate', file: 'duck-debate/mcp-app.html' },
|
|
609
|
+
{ name: 'Usage Stats', uri: 'ui://rubber-duck/usage-stats', file: 'usage-stats/mcp-app.html' },
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
for (const app of uiApps) {
|
|
613
|
+
registerAppResource(
|
|
614
|
+
this.server,
|
|
615
|
+
app.name,
|
|
616
|
+
app.uri,
|
|
617
|
+
{ description: `Interactive UI for ${app.name}` },
|
|
618
|
+
() => {
|
|
619
|
+
let html: string;
|
|
620
|
+
try {
|
|
621
|
+
html = readFileSync(join(uiDir, app.file), 'utf-8');
|
|
622
|
+
} catch {
|
|
623
|
+
html = `<html><body><p>UI not built. Run npm run build:ui</p></body></html>`;
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
contents: [
|
|
627
|
+
{
|
|
628
|
+
uri: app.uri,
|
|
629
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
630
|
+
text: html,
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
}
|
|
278
637
|
}
|
|
279
638
|
|
|
280
639
|
// MCP-enhanced tool handlers
|
|
@@ -311,7 +670,7 @@ export class RubberDuckServer {
|
|
|
311
670
|
return {
|
|
312
671
|
content: [
|
|
313
672
|
{
|
|
314
|
-
type: 'text',
|
|
673
|
+
type: 'text' as const,
|
|
315
674
|
text: formattedResponse,
|
|
316
675
|
},
|
|
317
676
|
],
|
|
@@ -337,12 +696,32 @@ export class RubberDuckServer {
|
|
|
337
696
|
.map((response) => this.formatEnhancedDuckResponse(response))
|
|
338
697
|
.join('\n\n═══════════════════════════════════════\n\n');
|
|
339
698
|
|
|
699
|
+
// Build structured data for UI consumption (same shape as compareDucksTool)
|
|
700
|
+
const structuredData = responses.map(r => ({
|
|
701
|
+
provider: r.provider,
|
|
702
|
+
nickname: r.nickname,
|
|
703
|
+
model: r.model,
|
|
704
|
+
content: r.content,
|
|
705
|
+
latency: r.latency,
|
|
706
|
+
tokens: r.usage ? {
|
|
707
|
+
prompt: r.usage.prompt_tokens,
|
|
708
|
+
completion: r.usage.completion_tokens,
|
|
709
|
+
total: r.usage.total_tokens,
|
|
710
|
+
} : null,
|
|
711
|
+
cached: r.cached,
|
|
712
|
+
error: r.content.startsWith('Error:') ? r.content : undefined,
|
|
713
|
+
}));
|
|
714
|
+
|
|
340
715
|
return {
|
|
341
716
|
content: [
|
|
342
717
|
{
|
|
343
|
-
type: 'text',
|
|
718
|
+
type: 'text' as const,
|
|
344
719
|
text: formattedResponse,
|
|
345
720
|
},
|
|
721
|
+
{
|
|
722
|
+
type: 'text' as const,
|
|
723
|
+
text: JSON.stringify(structuredData),
|
|
724
|
+
},
|
|
346
725
|
],
|
|
347
726
|
};
|
|
348
727
|
}
|
|
@@ -364,7 +743,7 @@ export class RubberDuckServer {
|
|
|
364
743
|
return {
|
|
365
744
|
content: [
|
|
366
745
|
{
|
|
367
|
-
type: 'text',
|
|
746
|
+
type: 'text' as const,
|
|
368
747
|
text: `${header}\n\n${formattedResponse}`,
|
|
369
748
|
},
|
|
370
749
|
],
|
|
@@ -412,417 +791,6 @@ export class RubberDuckServer {
|
|
|
412
791
|
return formatted;
|
|
413
792
|
}
|
|
414
793
|
|
|
415
|
-
private getTools(): Tool[] {
|
|
416
|
-
const baseTools: Tool[] = [
|
|
417
|
-
{
|
|
418
|
-
name: 'ask_duck',
|
|
419
|
-
description: this.mcpEnabled
|
|
420
|
-
? 'Ask a question to a specific LLM provider (duck) with MCP tool access'
|
|
421
|
-
: 'Ask a question to a specific LLM provider (duck)',
|
|
422
|
-
inputSchema: {
|
|
423
|
-
type: 'object',
|
|
424
|
-
properties: {
|
|
425
|
-
prompt: {
|
|
426
|
-
type: 'string',
|
|
427
|
-
description: 'The question or prompt to send to the duck',
|
|
428
|
-
},
|
|
429
|
-
provider: {
|
|
430
|
-
type: 'string',
|
|
431
|
-
description: 'The provider name (optional, uses default if not specified)',
|
|
432
|
-
},
|
|
433
|
-
model: {
|
|
434
|
-
type: 'string',
|
|
435
|
-
description:
|
|
436
|
-
'Specific model to use (optional, uses provider default if not specified)',
|
|
437
|
-
},
|
|
438
|
-
temperature: {
|
|
439
|
-
type: 'number',
|
|
440
|
-
description: 'Temperature for response generation (0-2)',
|
|
441
|
-
minimum: 0,
|
|
442
|
-
maximum: 2,
|
|
443
|
-
},
|
|
444
|
-
},
|
|
445
|
-
required: ['prompt'],
|
|
446
|
-
},
|
|
447
|
-
annotations: {
|
|
448
|
-
readOnlyHint: true,
|
|
449
|
-
openWorldHint: true,
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
{
|
|
453
|
-
name: 'chat_with_duck',
|
|
454
|
-
description: 'Have a conversation with a duck, maintaining context across messages',
|
|
455
|
-
inputSchema: {
|
|
456
|
-
type: 'object',
|
|
457
|
-
properties: {
|
|
458
|
-
conversation_id: {
|
|
459
|
-
type: 'string',
|
|
460
|
-
description: 'Conversation ID (creates new if not exists)',
|
|
461
|
-
},
|
|
462
|
-
message: {
|
|
463
|
-
type: 'string',
|
|
464
|
-
description: 'Your message to the duck',
|
|
465
|
-
},
|
|
466
|
-
provider: {
|
|
467
|
-
type: 'string',
|
|
468
|
-
description: 'Provider to use (can switch mid-conversation)',
|
|
469
|
-
},
|
|
470
|
-
model: {
|
|
471
|
-
type: 'string',
|
|
472
|
-
description: 'Specific model to use (optional)',
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
|
-
required: ['conversation_id', 'message'],
|
|
476
|
-
},
|
|
477
|
-
annotations: {
|
|
478
|
-
openWorldHint: true,
|
|
479
|
-
},
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
name: 'clear_conversations',
|
|
483
|
-
description: 'Clear all conversation history and start fresh',
|
|
484
|
-
inputSchema: {
|
|
485
|
-
type: 'object',
|
|
486
|
-
properties: {},
|
|
487
|
-
},
|
|
488
|
-
annotations: {
|
|
489
|
-
destructiveHint: true,
|
|
490
|
-
idempotentHint: true,
|
|
491
|
-
openWorldHint: false,
|
|
492
|
-
},
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
name: 'list_ducks',
|
|
496
|
-
description: 'List all available LLM providers (ducks) and their status',
|
|
497
|
-
inputSchema: {
|
|
498
|
-
type: 'object',
|
|
499
|
-
properties: {
|
|
500
|
-
check_health: {
|
|
501
|
-
type: 'boolean',
|
|
502
|
-
description: 'Perform health check on all providers',
|
|
503
|
-
default: false,
|
|
504
|
-
},
|
|
505
|
-
},
|
|
506
|
-
},
|
|
507
|
-
annotations: {
|
|
508
|
-
readOnlyHint: true,
|
|
509
|
-
openWorldHint: true,
|
|
510
|
-
},
|
|
511
|
-
},
|
|
512
|
-
{
|
|
513
|
-
name: 'list_models',
|
|
514
|
-
description: 'List available models for LLM providers',
|
|
515
|
-
inputSchema: {
|
|
516
|
-
type: 'object',
|
|
517
|
-
properties: {
|
|
518
|
-
provider: {
|
|
519
|
-
type: 'string',
|
|
520
|
-
description: 'Provider name (optional, lists all if not specified)',
|
|
521
|
-
},
|
|
522
|
-
fetch_latest: {
|
|
523
|
-
type: 'boolean',
|
|
524
|
-
description: 'Fetch latest models from API vs using cached/configured',
|
|
525
|
-
default: false,
|
|
526
|
-
},
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
annotations: {
|
|
530
|
-
readOnlyHint: true,
|
|
531
|
-
openWorldHint: true,
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
name: 'compare_ducks',
|
|
536
|
-
description: 'Ask the same question to multiple ducks simultaneously',
|
|
537
|
-
inputSchema: {
|
|
538
|
-
type: 'object',
|
|
539
|
-
properties: {
|
|
540
|
-
prompt: {
|
|
541
|
-
type: 'string',
|
|
542
|
-
description: 'The question to ask all ducks',
|
|
543
|
-
},
|
|
544
|
-
providers: {
|
|
545
|
-
type: 'array',
|
|
546
|
-
items: {
|
|
547
|
-
type: 'string',
|
|
548
|
-
},
|
|
549
|
-
description: 'List of provider names to query (optional, uses all if not specified)',
|
|
550
|
-
},
|
|
551
|
-
model: {
|
|
552
|
-
type: 'string',
|
|
553
|
-
description: 'Specific model to use for all providers (optional)',
|
|
554
|
-
},
|
|
555
|
-
},
|
|
556
|
-
required: ['prompt'],
|
|
557
|
-
},
|
|
558
|
-
annotations: {
|
|
559
|
-
readOnlyHint: true,
|
|
560
|
-
openWorldHint: true,
|
|
561
|
-
},
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
name: 'duck_council',
|
|
565
|
-
description: 'Get responses from all configured ducks (like a panel discussion)',
|
|
566
|
-
inputSchema: {
|
|
567
|
-
type: 'object',
|
|
568
|
-
properties: {
|
|
569
|
-
prompt: {
|
|
570
|
-
type: 'string',
|
|
571
|
-
description: 'The question for the duck council',
|
|
572
|
-
},
|
|
573
|
-
model: {
|
|
574
|
-
type: 'string',
|
|
575
|
-
description: 'Specific model to use for all ducks (optional)',
|
|
576
|
-
},
|
|
577
|
-
},
|
|
578
|
-
required: ['prompt'],
|
|
579
|
-
},
|
|
580
|
-
annotations: {
|
|
581
|
-
readOnlyHint: true,
|
|
582
|
-
openWorldHint: true,
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
{
|
|
586
|
-
name: 'duck_vote',
|
|
587
|
-
description: 'Have multiple ducks vote on options with reasoning. Returns vote tally, confidence scores, and consensus level.',
|
|
588
|
-
inputSchema: {
|
|
589
|
-
type: 'object',
|
|
590
|
-
properties: {
|
|
591
|
-
question: {
|
|
592
|
-
type: 'string',
|
|
593
|
-
description: 'The question to vote on (e.g., "Best approach for error handling?")',
|
|
594
|
-
},
|
|
595
|
-
options: {
|
|
596
|
-
type: 'array',
|
|
597
|
-
items: { type: 'string' },
|
|
598
|
-
minItems: 2,
|
|
599
|
-
maxItems: 10,
|
|
600
|
-
description: 'The options to vote on (2-10 options)',
|
|
601
|
-
},
|
|
602
|
-
voters: {
|
|
603
|
-
type: 'array',
|
|
604
|
-
items: { type: 'string' },
|
|
605
|
-
description: 'List of provider names to vote (optional, uses all if not specified)',
|
|
606
|
-
},
|
|
607
|
-
require_reasoning: {
|
|
608
|
-
type: 'boolean',
|
|
609
|
-
default: true,
|
|
610
|
-
description: 'Require ducks to explain their vote (default: true)',
|
|
611
|
-
},
|
|
612
|
-
},
|
|
613
|
-
required: ['question', 'options'],
|
|
614
|
-
},
|
|
615
|
-
annotations: {
|
|
616
|
-
readOnlyHint: true,
|
|
617
|
-
openWorldHint: true,
|
|
618
|
-
},
|
|
619
|
-
},
|
|
620
|
-
{
|
|
621
|
-
name: 'duck_judge',
|
|
622
|
-
description: 'Have one duck evaluate and rank other ducks\' responses. Use after duck_council to get a comparative evaluation.',
|
|
623
|
-
inputSchema: {
|
|
624
|
-
type: 'object',
|
|
625
|
-
properties: {
|
|
626
|
-
responses: {
|
|
627
|
-
type: 'array',
|
|
628
|
-
items: {
|
|
629
|
-
type: 'object',
|
|
630
|
-
properties: {
|
|
631
|
-
provider: { type: 'string' },
|
|
632
|
-
nickname: { type: 'string' },
|
|
633
|
-
model: { type: 'string' },
|
|
634
|
-
content: { type: 'string' },
|
|
635
|
-
},
|
|
636
|
-
required: ['provider', 'nickname', 'content'],
|
|
637
|
-
},
|
|
638
|
-
minItems: 2,
|
|
639
|
-
description: 'Array of duck responses to evaluate (from duck_council output)',
|
|
640
|
-
},
|
|
641
|
-
judge: {
|
|
642
|
-
type: 'string',
|
|
643
|
-
description: 'Provider name of the judge duck (optional, uses first available)',
|
|
644
|
-
},
|
|
645
|
-
criteria: {
|
|
646
|
-
type: 'array',
|
|
647
|
-
items: { type: 'string' },
|
|
648
|
-
description: 'Evaluation criteria (default: ["accuracy", "completeness", "clarity"])',
|
|
649
|
-
},
|
|
650
|
-
persona: {
|
|
651
|
-
type: 'string',
|
|
652
|
-
description: 'Judge persona (e.g., "senior engineer", "security expert")',
|
|
653
|
-
},
|
|
654
|
-
},
|
|
655
|
-
required: ['responses'],
|
|
656
|
-
},
|
|
657
|
-
annotations: {
|
|
658
|
-
readOnlyHint: true,
|
|
659
|
-
openWorldHint: true,
|
|
660
|
-
},
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
name: 'duck_iterate',
|
|
664
|
-
description: 'Iteratively refine a response between two ducks. One generates, the other critiques/improves, alternating for multiple rounds.',
|
|
665
|
-
inputSchema: {
|
|
666
|
-
type: 'object',
|
|
667
|
-
properties: {
|
|
668
|
-
prompt: {
|
|
669
|
-
type: 'string',
|
|
670
|
-
description: 'The initial prompt/task to iterate on',
|
|
671
|
-
},
|
|
672
|
-
iterations: {
|
|
673
|
-
type: 'number',
|
|
674
|
-
minimum: 1,
|
|
675
|
-
maximum: 10,
|
|
676
|
-
default: 3,
|
|
677
|
-
description: 'Number of iteration rounds (default: 3, max: 10)',
|
|
678
|
-
},
|
|
679
|
-
providers: {
|
|
680
|
-
type: 'array',
|
|
681
|
-
items: { type: 'string' },
|
|
682
|
-
minItems: 2,
|
|
683
|
-
maxItems: 2,
|
|
684
|
-
description: 'Exactly 2 provider names for the ping-pong iteration',
|
|
685
|
-
},
|
|
686
|
-
mode: {
|
|
687
|
-
type: 'string',
|
|
688
|
-
enum: ['refine', 'critique-improve'],
|
|
689
|
-
description: 'refine: each duck improves the previous response. critique-improve: alternates between critiquing and improving.',
|
|
690
|
-
},
|
|
691
|
-
},
|
|
692
|
-
required: ['prompt', 'providers', 'mode'],
|
|
693
|
-
},
|
|
694
|
-
annotations: {
|
|
695
|
-
readOnlyHint: true,
|
|
696
|
-
openWorldHint: true,
|
|
697
|
-
},
|
|
698
|
-
},
|
|
699
|
-
{
|
|
700
|
-
name: 'duck_debate',
|
|
701
|
-
description: 'Structured multi-round debate between ducks. Supports oxford (pro/con), socratic (questioning), and adversarial (attack/defend) formats.',
|
|
702
|
-
inputSchema: {
|
|
703
|
-
type: 'object',
|
|
704
|
-
properties: {
|
|
705
|
-
prompt: {
|
|
706
|
-
type: 'string',
|
|
707
|
-
description: 'The debate topic or proposition',
|
|
708
|
-
},
|
|
709
|
-
rounds: {
|
|
710
|
-
type: 'number',
|
|
711
|
-
minimum: 1,
|
|
712
|
-
maximum: 10,
|
|
713
|
-
default: 3,
|
|
714
|
-
description: 'Number of debate rounds (default: 3)',
|
|
715
|
-
},
|
|
716
|
-
providers: {
|
|
717
|
-
type: 'array',
|
|
718
|
-
items: { type: 'string' },
|
|
719
|
-
minItems: 2,
|
|
720
|
-
description: 'Provider names to participate (min 2, uses all if not specified)',
|
|
721
|
-
},
|
|
722
|
-
format: {
|
|
723
|
-
type: 'string',
|
|
724
|
-
enum: ['oxford', 'socratic', 'adversarial'],
|
|
725
|
-
description: 'Debate format: oxford (pro/con), socratic (questioning), adversarial (attack/defend)',
|
|
726
|
-
},
|
|
727
|
-
synthesizer: {
|
|
728
|
-
type: 'string',
|
|
729
|
-
description: 'Provider to synthesize the debate (optional, uses first provider)',
|
|
730
|
-
},
|
|
731
|
-
},
|
|
732
|
-
required: ['prompt', 'format'],
|
|
733
|
-
},
|
|
734
|
-
annotations: {
|
|
735
|
-
readOnlyHint: true,
|
|
736
|
-
openWorldHint: true,
|
|
737
|
-
},
|
|
738
|
-
},
|
|
739
|
-
{
|
|
740
|
-
name: 'get_usage_stats',
|
|
741
|
-
description:
|
|
742
|
-
'Get usage statistics for a time period. Shows token counts and costs (when pricing configured).',
|
|
743
|
-
inputSchema: {
|
|
744
|
-
type: 'object',
|
|
745
|
-
properties: {
|
|
746
|
-
period: {
|
|
747
|
-
type: 'string',
|
|
748
|
-
enum: ['today', '7d', '30d', 'all'],
|
|
749
|
-
default: 'today',
|
|
750
|
-
description: 'Time period for stats',
|
|
751
|
-
},
|
|
752
|
-
},
|
|
753
|
-
},
|
|
754
|
-
annotations: {
|
|
755
|
-
readOnlyHint: true,
|
|
756
|
-
openWorldHint: false,
|
|
757
|
-
},
|
|
758
|
-
},
|
|
759
|
-
];
|
|
760
|
-
|
|
761
|
-
// Add MCP-specific tools if enabled
|
|
762
|
-
if (this.mcpEnabled) {
|
|
763
|
-
baseTools.push(
|
|
764
|
-
{
|
|
765
|
-
name: 'get_pending_approvals',
|
|
766
|
-
description: 'Get list of pending MCP tool approvals from ducks',
|
|
767
|
-
inputSchema: {
|
|
768
|
-
type: 'object',
|
|
769
|
-
properties: {
|
|
770
|
-
duck: {
|
|
771
|
-
type: 'string',
|
|
772
|
-
description: 'Filter by duck name (optional)',
|
|
773
|
-
},
|
|
774
|
-
},
|
|
775
|
-
},
|
|
776
|
-
annotations: {
|
|
777
|
-
readOnlyHint: true,
|
|
778
|
-
openWorldHint: false,
|
|
779
|
-
},
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
name: 'approve_mcp_request',
|
|
783
|
-
description: "Approve or deny a duck's MCP tool request",
|
|
784
|
-
inputSchema: {
|
|
785
|
-
type: 'object',
|
|
786
|
-
properties: {
|
|
787
|
-
approval_id: {
|
|
788
|
-
type: 'string',
|
|
789
|
-
description: 'The approval request ID',
|
|
790
|
-
},
|
|
791
|
-
decision: {
|
|
792
|
-
type: 'string',
|
|
793
|
-
enum: ['approve', 'deny'],
|
|
794
|
-
description: 'Whether to approve or deny the request',
|
|
795
|
-
},
|
|
796
|
-
reason: {
|
|
797
|
-
type: 'string',
|
|
798
|
-
description: 'Reason for denial (optional)',
|
|
799
|
-
},
|
|
800
|
-
},
|
|
801
|
-
required: ['approval_id', 'decision'],
|
|
802
|
-
},
|
|
803
|
-
annotations: {
|
|
804
|
-
idempotentHint: true,
|
|
805
|
-
openWorldHint: false,
|
|
806
|
-
},
|
|
807
|
-
},
|
|
808
|
-
{
|
|
809
|
-
name: 'mcp_status',
|
|
810
|
-
description: 'Get status of MCP bridge, servers, and pending approvals',
|
|
811
|
-
inputSchema: {
|
|
812
|
-
type: 'object',
|
|
813
|
-
properties: {},
|
|
814
|
-
},
|
|
815
|
-
annotations: {
|
|
816
|
-
readOnlyHint: true,
|
|
817
|
-
openWorldHint: true,
|
|
818
|
-
},
|
|
819
|
-
}
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
return baseTools;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
794
|
async start() {
|
|
827
795
|
// Only show welcome message when not running as MCP server
|
|
828
796
|
const isMCP = process.env.MCP_SERVER === 'true' || process.argv.includes('--mcp');
|