mcp-rubber-duck 1.9.4 → 1.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.ts CHANGED
@@ -1,12 +1,7 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- ListPromptsRequestSchema,
7
- GetPromptRequestSchema,
8
- Tool,
9
- } from '@modelcontextprotocol/sdk/types.js';
3
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
4
+ import { z } from 'zod';
10
5
 
11
6
  import { ConfigManager } from './config/config.js';
12
7
  import { ProviderManager } from './providers/manager.js';
@@ -46,10 +41,10 @@ import { mcpStatusTool } from './tools/mcp-status.js';
46
41
  import { getUsageStatsTool } from './tools/get-usage-stats.js';
47
42
 
48
43
  // Import prompts
49
- import { getPrompts, getPrompt } from './prompts/index.js';
44
+ import { PROMPTS } from './prompts/index.js';
50
45
 
51
46
  export class RubberDuckServer {
52
- private server: Server;
47
+ private server: McpServer;
53
48
  private configManager: ConfigManager;
54
49
  private pricingService: PricingService;
55
50
  private usageService: UsageService;
@@ -67,17 +62,12 @@ export class RubberDuckServer {
67
62
  private mcpEnabled: boolean = false;
68
63
 
69
64
  constructor() {
70
- this.server = new Server(
65
+ this.server = new McpServer(
71
66
  {
72
67
  name: 'mcp-rubber-duck',
73
68
  version: '1.0.0',
74
69
  },
75
- {
76
- capabilities: {
77
- tools: {},
78
- prompts: {},
79
- },
80
- }
70
+ {}
81
71
  );
82
72
 
83
73
  // Initialize managers
@@ -102,7 +92,13 @@ export class RubberDuckServer {
102
92
  // Initialize MCP bridge if enabled
103
93
  this.initializeMCPBridge();
104
94
 
105
- this.setupHandlers();
95
+ this.registerTools();
96
+ this.registerPrompts();
97
+
98
+ // Handle errors
99
+ this.server.server.onerror = (error) => {
100
+ logger.error('Server error:', error);
101
+ };
106
102
  }
107
103
 
108
104
  private initializeMCPBridge(): void {
@@ -151,130 +147,438 @@ export class RubberDuckServer {
151
147
  }
152
148
  }
153
149
 
154
- private setupHandlers() {
155
- // List available tools
156
- this.server.setRequestHandler(ListToolsRequestSchema, () => {
157
- return { tools: this.getTools() };
158
- });
150
+ // Tool functions return `{ type: 'text' }` where TS infers `string`, not the literal `"text"`.
151
+ // This helper narrows the type to satisfy McpServer's CallToolResult expectation.
152
+ private toolResult(result: { content: { type: string; text: string }[]; isError?: boolean }): CallToolResult {
153
+ return result as CallToolResult;
154
+ }
159
155
 
160
- // List available prompts
161
- this.server.setRequestHandler(ListPromptsRequestSchema, () => {
162
- return { prompts: getPrompts() };
163
- });
156
+ private toolErrorResult(error: unknown): CallToolResult {
157
+ logger.error('Tool execution error:', error);
158
+ const errorMessage = error instanceof Error ? error.message : String(error);
159
+ return {
160
+ content: [
161
+ {
162
+ type: 'text' as const,
163
+ text: `${getRandomDuckMessage('error')}\n\nError: ${errorMessage}`,
164
+ },
165
+ ],
166
+ isError: true,
167
+ };
168
+ }
164
169
 
165
- // Get specific prompt
166
- this.server.setRequestHandler(GetPromptRequestSchema, (request) => {
167
- const { name, arguments: args } = request.params;
168
- try {
169
- return getPrompt(name, args || {});
170
- } catch (error: unknown) {
171
- const errorMessage = error instanceof Error ? error.message : String(error);
172
- logger.error(`Prompt error for ${name}:`, errorMessage);
173
- throw error;
170
+ private registerTools() {
171
+ // ask_duck
172
+ this.server.registerTool(
173
+ 'ask_duck',
174
+ {
175
+ description: this.mcpEnabled
176
+ ? 'Ask a question to a specific LLM provider (duck) with MCP tool access'
177
+ : 'Ask a question to a specific LLM provider (duck)',
178
+ inputSchema: {
179
+ prompt: z.string().describe('The question or prompt to send to the duck'),
180
+ provider: z.string().optional().describe('The provider name (optional, uses default if not specified)'),
181
+ model: z.string().optional().describe('Specific model to use (optional, uses provider default if not specified)'),
182
+ temperature: z.number().min(0).max(2).optional().describe('Temperature for response generation (0-2)'),
183
+ },
184
+ annotations: {
185
+ readOnlyHint: true,
186
+ openWorldHint: true,
187
+ },
188
+ },
189
+ async (args) => {
190
+ try {
191
+ if (this.mcpEnabled && this.enhancedProviderManager) {
192
+ return this.toolResult(await this.handleAskDuckWithMCP(args as Record<string, unknown>));
193
+ }
194
+ return this.toolResult(await askDuckTool(this.providerManager, this.cache, args as Record<string, unknown>));
195
+ } catch (error) {
196
+ return this.toolErrorResult(error);
197
+ }
174
198
  }
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 || {});
199
+ );
189
200
 
190
- case 'chat_with_duck':
191
- return await chatDuckTool(this.providerManager, this.conversationManager, args || {});
201
+ // chat_with_duck
202
+ this.server.registerTool(
203
+ 'chat_with_duck',
204
+ {
205
+ description: 'Have a conversation with a duck, maintaining context across messages',
206
+ inputSchema: {
207
+ conversation_id: z.string().describe('Conversation ID (creates new if not exists)'),
208
+ message: z.string().describe('Your message to the duck'),
209
+ provider: z.string().optional().describe('Provider to use (can switch mid-conversation)'),
210
+ model: z.string().optional().describe('Specific model to use (optional)'),
211
+ },
212
+ annotations: {
213
+ openWorldHint: true,
214
+ },
215
+ },
216
+ async (args) => {
217
+ try {
218
+ return this.toolResult(await chatDuckTool(this.providerManager, this.conversationManager, args as Record<string, unknown>));
219
+ } catch (error) {
220
+ return this.toolErrorResult(error);
221
+ }
222
+ }
223
+ );
192
224
 
193
- case 'clear_conversations':
194
- return clearConversationsTool(this.conversationManager, args || {});
225
+ // clear_conversations
226
+ this.server.registerTool(
227
+ 'clear_conversations',
228
+ {
229
+ description: 'Clear all conversation history and start fresh',
230
+ annotations: {
231
+ destructiveHint: true,
232
+ idempotentHint: true,
233
+ openWorldHint: false,
234
+ },
235
+ },
236
+ () => {
237
+ try {
238
+ return this.toolResult(clearConversationsTool(this.conversationManager, {}));
239
+ } catch (error) {
240
+ return this.toolErrorResult(error);
241
+ }
242
+ }
243
+ );
195
244
 
196
- case 'list_ducks':
197
- return await listDucksTool(this.providerManager, this.healthMonitor, args || {});
245
+ // list_ducks
246
+ this.server.registerTool(
247
+ 'list_ducks',
248
+ {
249
+ description: 'List all available LLM providers (ducks) and their status',
250
+ inputSchema: {
251
+ check_health: z.boolean().default(false).describe('Perform health check on all providers'),
252
+ },
253
+ annotations: {
254
+ readOnlyHint: true,
255
+ openWorldHint: true,
256
+ },
257
+ },
258
+ async (args) => {
259
+ try {
260
+ return this.toolResult(await listDucksTool(this.providerManager, this.healthMonitor, args as Record<string, unknown>));
261
+ } catch (error) {
262
+ return this.toolErrorResult(error);
263
+ }
264
+ }
265
+ );
198
266
 
199
- case 'list_models':
200
- return await listModelsTool(this.providerManager, args || {});
267
+ // list_models
268
+ this.server.registerTool(
269
+ 'list_models',
270
+ {
271
+ description: 'List available models for LLM providers',
272
+ inputSchema: {
273
+ provider: z.string().optional().describe('Provider name (optional, lists all if not specified)'),
274
+ fetch_latest: z.boolean().default(false).describe('Fetch latest models from API vs using cached/configured'),
275
+ },
276
+ annotations: {
277
+ readOnlyHint: true,
278
+ openWorldHint: true,
279
+ },
280
+ },
281
+ async (args) => {
282
+ try {
283
+ return this.toolResult(await listModelsTool(this.providerManager, args as Record<string, unknown>));
284
+ } catch (error) {
285
+ return this.toolErrorResult(error);
286
+ }
287
+ }
288
+ );
201
289
 
202
- case 'compare_ducks':
203
- // Use enhanced provider manager if MCP is enabled
204
- if (this.mcpEnabled && this.enhancedProviderManager) {
205
- return await this.handleCompareDucksWithMCP(args || {});
206
- }
207
- return await compareDucksTool(this.providerManager, args || {});
290
+ // compare_ducks
291
+ this.server.registerTool(
292
+ 'compare_ducks',
293
+ {
294
+ description: 'Ask the same question to multiple ducks simultaneously',
295
+ inputSchema: {
296
+ prompt: z.string().describe('The question to ask all ducks'),
297
+ providers: z.array(z.string()).optional().describe('List of provider names to query (optional, uses all if not specified)'),
298
+ model: z.string().optional().describe('Specific model to use for all providers (optional)'),
299
+ },
300
+ annotations: {
301
+ readOnlyHint: true,
302
+ openWorldHint: true,
303
+ },
304
+ },
305
+ async (args) => {
306
+ try {
307
+ if (this.mcpEnabled && this.enhancedProviderManager) {
308
+ return this.toolResult(await this.handleCompareDucksWithMCP(args as Record<string, unknown>));
309
+ }
310
+ return this.toolResult(await compareDucksTool(this.providerManager, args as Record<string, unknown>));
311
+ } catch (error) {
312
+ return this.toolErrorResult(error);
313
+ }
314
+ }
315
+ );
208
316
 
209
- case 'duck_council':
210
- // Use enhanced provider manager if MCP is enabled
211
- if (this.mcpEnabled && this.enhancedProviderManager) {
212
- return await this.handleDuckCouncilWithMCP(args || {});
213
- }
214
- return await duckCouncilTool(this.providerManager, args || {});
317
+ // duck_council
318
+ this.server.registerTool(
319
+ 'duck_council',
320
+ {
321
+ description: 'Get responses from all configured ducks (like a panel discussion)',
322
+ inputSchema: {
323
+ prompt: z.string().describe('The question for the duck council'),
324
+ model: z.string().optional().describe('Specific model to use for all ducks (optional)'),
325
+ },
326
+ annotations: {
327
+ readOnlyHint: true,
328
+ openWorldHint: true,
329
+ },
330
+ },
331
+ async (args) => {
332
+ try {
333
+ if (this.mcpEnabled && this.enhancedProviderManager) {
334
+ return this.toolResult(await this.handleDuckCouncilWithMCP(args as Record<string, unknown>));
335
+ }
336
+ return this.toolResult(await duckCouncilTool(this.providerManager, args as Record<string, unknown>));
337
+ } catch (error) {
338
+ return this.toolErrorResult(error);
339
+ }
340
+ }
341
+ );
215
342
 
216
- case 'duck_vote':
217
- return await duckVoteTool(this.providerManager, args || {});
343
+ // duck_vote
344
+ this.server.registerTool(
345
+ 'duck_vote',
346
+ {
347
+ description: 'Have multiple ducks vote on options with reasoning. Returns vote tally, confidence scores, and consensus level.',
348
+ inputSchema: {
349
+ question: z.string().describe('The question to vote on (e.g., "Best approach for error handling?")'),
350
+ options: z.array(z.string()).min(2).max(10).describe('The options to vote on (2-10 options)'),
351
+ voters: z.array(z.string()).optional().describe('List of provider names to vote (optional, uses all if not specified)'),
352
+ require_reasoning: z.boolean().default(true).describe('Require ducks to explain their vote (default: true)'),
353
+ },
354
+ annotations: {
355
+ readOnlyHint: true,
356
+ openWorldHint: true,
357
+ },
358
+ },
359
+ async (args) => {
360
+ try {
361
+ return this.toolResult(await duckVoteTool(this.providerManager, args as Record<string, unknown>));
362
+ } catch (error) {
363
+ return this.toolErrorResult(error);
364
+ }
365
+ }
366
+ );
218
367
 
219
- case 'duck_judge':
220
- return await duckJudgeTool(this.providerManager, args || {});
368
+ // duck_judge
369
+ this.server.registerTool(
370
+ 'duck_judge',
371
+ {
372
+ description: 'Have one duck evaluate and rank other ducks\' responses. Use after duck_council to get a comparative evaluation.',
373
+ inputSchema: {
374
+ responses: z.array(z.object({
375
+ provider: z.string(),
376
+ nickname: z.string(),
377
+ model: z.string().optional(),
378
+ content: z.string(),
379
+ })).min(2).describe('Array of duck responses to evaluate (from duck_council output)'),
380
+ judge: z.string().optional().describe('Provider name of the judge duck (optional, uses first available)'),
381
+ criteria: z.array(z.string()).optional().describe('Evaluation criteria (default: ["accuracy", "completeness", "clarity"])'),
382
+ persona: z.string().optional().describe('Judge persona (e.g., "senior engineer", "security expert")'),
383
+ },
384
+ annotations: {
385
+ readOnlyHint: true,
386
+ openWorldHint: true,
387
+ },
388
+ },
389
+ async (args) => {
390
+ try {
391
+ return this.toolResult(await duckJudgeTool(this.providerManager, args as Record<string, unknown>));
392
+ } catch (error) {
393
+ return this.toolErrorResult(error);
394
+ }
395
+ }
396
+ );
221
397
 
222
- case 'duck_iterate':
223
- return await duckIterateTool(this.providerManager, args || {});
398
+ // duck_iterate
399
+ this.server.registerTool(
400
+ 'duck_iterate',
401
+ {
402
+ description: 'Iteratively refine a response between two ducks. One generates, the other critiques/improves, alternating for multiple rounds.',
403
+ inputSchema: {
404
+ prompt: z.string().describe('The initial prompt/task to iterate on'),
405
+ iterations: z.number().min(1).max(10).default(3).describe('Number of iteration rounds (default: 3, max: 10)'),
406
+ providers: z.array(z.string()).min(2).max(2).describe('Exactly 2 provider names for the ping-pong iteration'),
407
+ mode: z.enum(['refine', 'critique-improve']).describe('refine: each duck improves the previous response. critique-improve: alternates between critiquing and improving.'),
408
+ },
409
+ annotations: {
410
+ readOnlyHint: true,
411
+ openWorldHint: true,
412
+ },
413
+ },
414
+ async (args) => {
415
+ try {
416
+ return this.toolResult(await duckIterateTool(this.providerManager, args as Record<string, unknown>));
417
+ } catch (error) {
418
+ return this.toolErrorResult(error);
419
+ }
420
+ }
421
+ );
224
422
 
225
- case 'duck_debate':
226
- return await duckDebateTool(this.providerManager, args || {});
423
+ // duck_debate
424
+ this.server.registerTool(
425
+ 'duck_debate',
426
+ {
427
+ description: 'Structured multi-round debate between ducks. Supports oxford (pro/con), socratic (questioning), and adversarial (attack/defend) formats.',
428
+ inputSchema: {
429
+ prompt: z.string().describe('The debate topic or proposition'),
430
+ rounds: z.number().min(1).max(10).default(3).describe('Number of debate rounds (default: 3)'),
431
+ providers: z.array(z.string()).min(2).optional().describe('Provider names to participate (min 2, uses all if not specified)'),
432
+ format: z.enum(['oxford', 'socratic', 'adversarial']).describe('Debate format: oxford (pro/con), socratic (questioning), adversarial (attack/defend)'),
433
+ synthesizer: z.string().optional().describe('Provider to synthesize the debate (optional, uses first provider)'),
434
+ },
435
+ annotations: {
436
+ readOnlyHint: true,
437
+ openWorldHint: true,
438
+ },
439
+ },
440
+ async (args) => {
441
+ try {
442
+ return this.toolResult(await duckDebateTool(this.providerManager, args as Record<string, unknown>));
443
+ } catch (error) {
444
+ return this.toolErrorResult(error);
445
+ }
446
+ }
447
+ );
227
448
 
228
- // Usage stats tool
229
- case 'get_usage_stats':
230
- return getUsageStatsTool(this.usageService, args || {});
449
+ // get_usage_stats
450
+ this.server.registerTool(
451
+ 'get_usage_stats',
452
+ {
453
+ description: 'Get usage statistics for a time period. Shows token counts and costs (when pricing configured).',
454
+ inputSchema: {
455
+ period: z.enum(['today', '7d', '30d', 'all']).default('today').describe('Time period for stats'),
456
+ },
457
+ annotations: {
458
+ readOnlyHint: true,
459
+ openWorldHint: false,
460
+ },
461
+ },
462
+ (args) => {
463
+ try {
464
+ return this.toolResult(getUsageStatsTool(this.usageService, args as Record<string, unknown>));
465
+ } catch (error) {
466
+ return this.toolErrorResult(error);
467
+ }
468
+ }
469
+ );
231
470
 
232
- // MCP-specific tools
233
- case 'get_pending_approvals':
471
+ // Conditionally register MCP tools
472
+ if (this.mcpEnabled) {
473
+ // get_pending_approvals
474
+ this.server.registerTool(
475
+ 'get_pending_approvals',
476
+ {
477
+ description: 'Get list of pending MCP tool approvals from ducks',
478
+ inputSchema: {
479
+ duck: z.string().optional().describe('Filter by duck name (optional)'),
480
+ },
481
+ annotations: {
482
+ readOnlyHint: true,
483
+ openWorldHint: false,
484
+ },
485
+ },
486
+ (args) => {
487
+ try {
234
488
  if (!this.approvalService) {
235
489
  throw new Error('MCP bridge not enabled');
236
490
  }
237
- return getPendingApprovalsTool(this.approvalService, args || {});
491
+ return this.toolResult(getPendingApprovalsTool(this.approvalService, args as Record<string, unknown>));
492
+ } catch (error) {
493
+ return this.toolErrorResult(error);
494
+ }
495
+ }
496
+ );
238
497
 
239
- case 'approve_mcp_request':
498
+ // approve_mcp_request
499
+ this.server.registerTool(
500
+ 'approve_mcp_request',
501
+ {
502
+ description: "Approve or deny a duck's MCP tool request",
503
+ inputSchema: {
504
+ approval_id: z.string().describe('The approval request ID'),
505
+ decision: z.enum(['approve', 'deny']).describe('Whether to approve or deny the request'),
506
+ reason: z.string().optional().describe('Reason for denial (optional)'),
507
+ },
508
+ annotations: {
509
+ idempotentHint: true,
510
+ openWorldHint: false,
511
+ },
512
+ },
513
+ (args) => {
514
+ try {
240
515
  if (!this.approvalService) {
241
516
  throw new Error('MCP bridge not enabled');
242
517
  }
243
- return approveMCPRequestTool(this.approvalService, args || {});
518
+ return this.toolResult(approveMCPRequestTool(this.approvalService, args as Record<string, unknown>));
519
+ } catch (error) {
520
+ return this.toolErrorResult(error);
521
+ }
522
+ }
523
+ );
244
524
 
245
- case 'mcp_status':
525
+ // mcp_status
526
+ this.server.registerTool(
527
+ 'mcp_status',
528
+ {
529
+ description: 'Get status of MCP bridge, servers, and pending approvals',
530
+ annotations: {
531
+ readOnlyHint: true,
532
+ openWorldHint: true,
533
+ },
534
+ },
535
+ async () => {
536
+ try {
246
537
  if (!this.mcpClientManager || !this.approvalService || !this.functionBridge) {
247
538
  throw new Error('MCP bridge not enabled');
248
539
  }
249
- return await mcpStatusTool(
540
+ return this.toolResult(await mcpStatusTool(
250
541
  this.mcpClientManager,
251
542
  this.approvalService,
252
543
  this.functionBridge,
253
- args || {}
254
- );
255
-
256
- default:
257
- throw new Error(`Unknown tool: ${name}`);
544
+ {}
545
+ ));
546
+ } catch (error) {
547
+ return this.toolErrorResult(error);
548
+ }
258
549
  }
259
- } catch (error: unknown) {
260
- logger.error(`Tool execution error for ${name}:`, error);
261
- const errorMessage = error instanceof Error ? error.message : String(error);
262
- return {
263
- content: [
264
- {
265
- type: 'text',
266
- text: `${getRandomDuckMessage('error')}\n\nError: ${errorMessage}`,
267
- },
268
- ],
269
- isError: true,
270
- };
550
+ );
551
+ }
552
+ }
553
+
554
+ private registerPrompts() {
555
+ for (const [name, prompt] of Object.entries(PROMPTS)) {
556
+ // Convert prompt.arguments array to Zod schema
557
+ const argsSchema: Record<string, z.ZodType> = {};
558
+ for (const arg of prompt.arguments || []) {
559
+ argsSchema[arg.name] = arg.required
560
+ ? z.string().describe(arg.description || '')
561
+ : z.string().optional().describe(arg.description || '');
271
562
  }
272
- });
273
563
 
274
- // Handle errors
275
- this.server.onerror = (error) => {
276
- logger.error('Server error:', error);
277
- };
564
+ this.server.registerPrompt(
565
+ name,
566
+ {
567
+ description: prompt.description,
568
+ argsSchema,
569
+ },
570
+ (args) => {
571
+ try {
572
+ const messages = prompt.buildMessages((args || {}) as Record<string, string>);
573
+ return { messages };
574
+ } catch (error: unknown) {
575
+ const errorMessage = error instanceof Error ? error.message : String(error);
576
+ logger.error(`Prompt error for ${name}:`, errorMessage);
577
+ throw error;
578
+ }
579
+ }
580
+ );
581
+ }
278
582
  }
279
583
 
280
584
  // MCP-enhanced tool handlers
@@ -311,7 +615,7 @@ export class RubberDuckServer {
311
615
  return {
312
616
  content: [
313
617
  {
314
- type: 'text',
618
+ type: 'text' as const,
315
619
  text: formattedResponse,
316
620
  },
317
621
  ],
@@ -340,7 +644,7 @@ export class RubberDuckServer {
340
644
  return {
341
645
  content: [
342
646
  {
343
- type: 'text',
647
+ type: 'text' as const,
344
648
  text: formattedResponse,
345
649
  },
346
650
  ],
@@ -364,7 +668,7 @@ export class RubberDuckServer {
364
668
  return {
365
669
  content: [
366
670
  {
367
- type: 'text',
671
+ type: 'text' as const,
368
672
  text: `${header}\n\n${formattedResponse}`,
369
673
  },
370
674
  ],
@@ -412,417 +716,6 @@ export class RubberDuckServer {
412
716
  return formatted;
413
717
  }
414
718
 
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
719
  async start() {
827
720
  // Only show welcome message when not running as MCP server
828
721
  const isMCP = process.env.MCP_SERVER === 'true' || process.argv.includes('--mcp');