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