mcp-rubber-duck 1.1.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.
Files changed (184) hide show
  1. package/.dockerignore +19 -0
  2. package/.env.desktop.example +145 -0
  3. package/.env.example +45 -0
  4. package/.env.pi.example +106 -0
  5. package/.env.template +165 -0
  6. package/.eslintrc.json +40 -0
  7. package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
  9. package/.github/ISSUE_TEMPLATE/question.md +67 -0
  10. package/.github/pull_request_template.md +111 -0
  11. package/.github/workflows/docker-build.yml +138 -0
  12. package/.github/workflows/release.yml +182 -0
  13. package/.github/workflows/security.yml +141 -0
  14. package/.github/workflows/semantic-release.yml +89 -0
  15. package/.prettierrc +10 -0
  16. package/.releaserc.json +66 -0
  17. package/CHANGELOG.md +95 -0
  18. package/CONTRIBUTING.md +242 -0
  19. package/Dockerfile +62 -0
  20. package/LICENSE +21 -0
  21. package/README.md +803 -0
  22. package/audit-ci.json +8 -0
  23. package/config/claude_desktop.json +14 -0
  24. package/config/config.example.json +91 -0
  25. package/dist/config/config.d.ts +51 -0
  26. package/dist/config/config.d.ts.map +1 -0
  27. package/dist/config/config.js +301 -0
  28. package/dist/config/config.js.map +1 -0
  29. package/dist/config/types.d.ts +356 -0
  30. package/dist/config/types.d.ts.map +1 -0
  31. package/dist/config/types.js +41 -0
  32. package/dist/config/types.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +109 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/providers/duck-provider-enhanced.d.ts +29 -0
  38. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
  39. package/dist/providers/duck-provider-enhanced.js +230 -0
  40. package/dist/providers/duck-provider-enhanced.js.map +1 -0
  41. package/dist/providers/enhanced-manager.d.ts +54 -0
  42. package/dist/providers/enhanced-manager.d.ts.map +1 -0
  43. package/dist/providers/enhanced-manager.js +217 -0
  44. package/dist/providers/enhanced-manager.js.map +1 -0
  45. package/dist/providers/manager.d.ts +28 -0
  46. package/dist/providers/manager.d.ts.map +1 -0
  47. package/dist/providers/manager.js +204 -0
  48. package/dist/providers/manager.js.map +1 -0
  49. package/dist/providers/provider.d.ts +29 -0
  50. package/dist/providers/provider.d.ts.map +1 -0
  51. package/dist/providers/provider.js +179 -0
  52. package/dist/providers/provider.js.map +1 -0
  53. package/dist/providers/types.d.ts +69 -0
  54. package/dist/providers/types.d.ts.map +1 -0
  55. package/dist/providers/types.js +2 -0
  56. package/dist/providers/types.js.map +1 -0
  57. package/dist/server.d.ts +24 -0
  58. package/dist/server.d.ts.map +1 -0
  59. package/dist/server.js +501 -0
  60. package/dist/server.js.map +1 -0
  61. package/dist/services/approval.d.ts +44 -0
  62. package/dist/services/approval.d.ts.map +1 -0
  63. package/dist/services/approval.js +159 -0
  64. package/dist/services/approval.js.map +1 -0
  65. package/dist/services/cache.d.ts +21 -0
  66. package/dist/services/cache.d.ts.map +1 -0
  67. package/dist/services/cache.js +63 -0
  68. package/dist/services/cache.js.map +1 -0
  69. package/dist/services/conversation.d.ts +24 -0
  70. package/dist/services/conversation.d.ts.map +1 -0
  71. package/dist/services/conversation.js +108 -0
  72. package/dist/services/conversation.js.map +1 -0
  73. package/dist/services/function-bridge.d.ts +41 -0
  74. package/dist/services/function-bridge.d.ts.map +1 -0
  75. package/dist/services/function-bridge.js +259 -0
  76. package/dist/services/function-bridge.js.map +1 -0
  77. package/dist/services/health.d.ts +17 -0
  78. package/dist/services/health.d.ts.map +1 -0
  79. package/dist/services/health.js +77 -0
  80. package/dist/services/health.js.map +1 -0
  81. package/dist/services/mcp-client-manager.d.ts +49 -0
  82. package/dist/services/mcp-client-manager.d.ts.map +1 -0
  83. package/dist/services/mcp-client-manager.js +279 -0
  84. package/dist/services/mcp-client-manager.js.map +1 -0
  85. package/dist/tools/approve-mcp-request.d.ts +9 -0
  86. package/dist/tools/approve-mcp-request.d.ts.map +1 -0
  87. package/dist/tools/approve-mcp-request.js +111 -0
  88. package/dist/tools/approve-mcp-request.js.map +1 -0
  89. package/dist/tools/ask-duck.d.ts +9 -0
  90. package/dist/tools/ask-duck.d.ts.map +1 -0
  91. package/dist/tools/ask-duck.js +43 -0
  92. package/dist/tools/ask-duck.js.map +1 -0
  93. package/dist/tools/chat-duck.d.ts +9 -0
  94. package/dist/tools/chat-duck.d.ts.map +1 -0
  95. package/dist/tools/chat-duck.js +57 -0
  96. package/dist/tools/chat-duck.js.map +1 -0
  97. package/dist/tools/clear-conversations.d.ts +8 -0
  98. package/dist/tools/clear-conversations.d.ts.map +1 -0
  99. package/dist/tools/clear-conversations.js +17 -0
  100. package/dist/tools/clear-conversations.js.map +1 -0
  101. package/dist/tools/compare-ducks.d.ts +8 -0
  102. package/dist/tools/compare-ducks.d.ts.map +1 -0
  103. package/dist/tools/compare-ducks.js +49 -0
  104. package/dist/tools/compare-ducks.js.map +1 -0
  105. package/dist/tools/duck-council.d.ts +8 -0
  106. package/dist/tools/duck-council.d.ts.map +1 -0
  107. package/dist/tools/duck-council.js +69 -0
  108. package/dist/tools/duck-council.js.map +1 -0
  109. package/dist/tools/get-pending-approvals.d.ts +15 -0
  110. package/dist/tools/get-pending-approvals.d.ts.map +1 -0
  111. package/dist/tools/get-pending-approvals.js +74 -0
  112. package/dist/tools/get-pending-approvals.js.map +1 -0
  113. package/dist/tools/list-ducks.d.ts +9 -0
  114. package/dist/tools/list-ducks.d.ts.map +1 -0
  115. package/dist/tools/list-ducks.js +47 -0
  116. package/dist/tools/list-ducks.js.map +1 -0
  117. package/dist/tools/list-models.d.ts +8 -0
  118. package/dist/tools/list-models.d.ts.map +1 -0
  119. package/dist/tools/list-models.js +72 -0
  120. package/dist/tools/list-models.js.map +1 -0
  121. package/dist/tools/mcp-status.d.ts +17 -0
  122. package/dist/tools/mcp-status.d.ts.map +1 -0
  123. package/dist/tools/mcp-status.js +100 -0
  124. package/dist/tools/mcp-status.js.map +1 -0
  125. package/dist/utils/ascii-art.d.ts +19 -0
  126. package/dist/utils/ascii-art.d.ts.map +1 -0
  127. package/dist/utils/ascii-art.js +73 -0
  128. package/dist/utils/ascii-art.js.map +1 -0
  129. package/dist/utils/logger.d.ts +3 -0
  130. package/dist/utils/logger.d.ts.map +1 -0
  131. package/dist/utils/logger.js +86 -0
  132. package/dist/utils/logger.js.map +1 -0
  133. package/dist/utils/safe-logger.d.ts +23 -0
  134. package/dist/utils/safe-logger.d.ts.map +1 -0
  135. package/dist/utils/safe-logger.js +145 -0
  136. package/dist/utils/safe-logger.js.map +1 -0
  137. package/docker-compose.yml +161 -0
  138. package/jest.config.js +26 -0
  139. package/package.json +65 -0
  140. package/scripts/build-multiarch.sh +290 -0
  141. package/scripts/deploy-raspbian.sh +410 -0
  142. package/scripts/deploy.sh +322 -0
  143. package/scripts/gh-deploy.sh +343 -0
  144. package/scripts/setup-docker-raspbian.sh +530 -0
  145. package/server.json +8 -0
  146. package/src/config/config.ts +357 -0
  147. package/src/config/types.ts +89 -0
  148. package/src/index.ts +114 -0
  149. package/src/providers/duck-provider-enhanced.ts +294 -0
  150. package/src/providers/enhanced-manager.ts +290 -0
  151. package/src/providers/manager.ts +257 -0
  152. package/src/providers/provider.ts +207 -0
  153. package/src/providers/types.ts +78 -0
  154. package/src/server.ts +603 -0
  155. package/src/services/approval.ts +225 -0
  156. package/src/services/cache.ts +79 -0
  157. package/src/services/conversation.ts +146 -0
  158. package/src/services/function-bridge.ts +329 -0
  159. package/src/services/health.ts +107 -0
  160. package/src/services/mcp-client-manager.ts +362 -0
  161. package/src/tools/approve-mcp-request.ts +126 -0
  162. package/src/tools/ask-duck.ts +74 -0
  163. package/src/tools/chat-duck.ts +82 -0
  164. package/src/tools/clear-conversations.ts +24 -0
  165. package/src/tools/compare-ducks.ts +67 -0
  166. package/src/tools/duck-council.ts +88 -0
  167. package/src/tools/get-pending-approvals.ts +90 -0
  168. package/src/tools/list-ducks.ts +65 -0
  169. package/src/tools/list-models.ts +101 -0
  170. package/src/tools/mcp-status.ts +117 -0
  171. package/src/utils/ascii-art.ts +85 -0
  172. package/src/utils/logger.ts +116 -0
  173. package/src/utils/safe-logger.ts +165 -0
  174. package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
  175. package/systemd/mcp-rubber-duck.service +58 -0
  176. package/test-functionality.js +147 -0
  177. package/test-mcp-interface.js +221 -0
  178. package/tests/ascii-art.test.ts +36 -0
  179. package/tests/config.test.ts +239 -0
  180. package/tests/conversation.test.ts +308 -0
  181. package/tests/mcp-bridge.test.ts +291 -0
  182. package/tests/providers.test.ts +269 -0
  183. package/tests/tools/clear-conversations.test.ts +163 -0
  184. package/tsconfig.json +26 -0
@@ -0,0 +1,294 @@
1
+ import { DuckProvider } from './provider.js';
2
+ import { ChatOptions, ChatResponse, ProviderOptions, OpenAIChatParams, OpenAIMessage, MCPResult, OpenAIToolCall } from './types.js';
3
+ import { FunctionBridge } from '../services/function-bridge.js';
4
+ import { ConversationMessage } from '../config/types.js';
5
+ import { logger } from '../utils/logger.js';
6
+ import { SafeLogger } from '../utils/safe-logger.js';
7
+
8
+ export interface EnhancedChatResponse extends ChatResponse {
9
+ pendingApprovals?: {
10
+ id: string;
11
+ message: string;
12
+ }[];
13
+ mcpResults?: MCPResult[];
14
+ }
15
+
16
+ export class EnhancedDuckProvider extends DuckProvider {
17
+ private functionBridge: FunctionBridge;
18
+ private mcpEnabled: boolean;
19
+
20
+ constructor(
21
+ name: string,
22
+ nickname: string,
23
+ options: ProviderOptions,
24
+ functionBridge: FunctionBridge,
25
+ mcpEnabled: boolean = true
26
+ ) {
27
+ super(name, nickname, options);
28
+ this.functionBridge = functionBridge;
29
+ this.mcpEnabled = mcpEnabled;
30
+ }
31
+
32
+ async chat(options: ChatOptions): Promise<EnhancedChatResponse> {
33
+ try {
34
+ // If MCP is enabled, add function definitions
35
+ if (this.mcpEnabled) {
36
+ const functions = await this.functionBridge.getFunctionDefinitions();
37
+ if (functions.length > 0) {
38
+ options.tools = functions;
39
+ options.toolChoice = 'auto';
40
+ logger.debug(`Added ${functions.length} MCP functions for ${this.nickname}`);
41
+ }
42
+ }
43
+
44
+ // Prepare messages for function calling
45
+ const messages = this.prepareMessages(options.messages, options.systemPrompt);
46
+ const modelToUse = options.model || this.options.model;
47
+
48
+ const baseParams: Partial<OpenAIChatParams> = {
49
+ model: modelToUse,
50
+ messages: messages as OpenAIMessage[],
51
+ stream: false,
52
+ };
53
+
54
+ // Add temperature if model supports it
55
+ if (this.supportsTemperature(modelToUse)) {
56
+ baseParams.temperature = options.temperature ?? this.options.temperature ?? 0.7;
57
+ }
58
+
59
+ // Add tools if available
60
+ if (options.tools && options.tools.length > 0) {
61
+ baseParams.tools = options.tools.map(tool => ({
62
+ type: 'function',
63
+ function: {
64
+ name: tool.name,
65
+ description: tool.description,
66
+ parameters: tool.parameters,
67
+ },
68
+ }));
69
+ baseParams.tool_choice = options.toolChoice || 'auto';
70
+ }
71
+
72
+ // First API call
73
+ const response = await this.createChatCompletion(baseParams);
74
+ const choice = response.choices[0];
75
+
76
+ // Check if the model wants to call functions
77
+ if (choice.message?.tool_calls && choice.message.tool_calls.length > 0) {
78
+ return await this.handleToolCalls(
79
+ choice.message.tool_calls,
80
+ messages as OpenAIMessage[],
81
+ baseParams,
82
+ modelToUse
83
+ );
84
+ }
85
+
86
+ // No tool calls, return regular response
87
+ return {
88
+ content: choice.message?.content || '',
89
+ usage: response.usage ? {
90
+ promptTokens: response.usage.prompt_tokens,
91
+ completionTokens: response.usage.completion_tokens,
92
+ totalTokens: response.usage.total_tokens,
93
+ } : undefined,
94
+ model: modelToUse,
95
+ finishReason: choice.finish_reason || undefined,
96
+ };
97
+
98
+ } catch (error: unknown) {
99
+ logger.error(`Enhanced provider ${this.name} chat error:`, error);
100
+ const errorMessage = error instanceof Error ? error.message : String(error);
101
+ throw new Error(`Duck ${this.nickname} couldn't respond: ${errorMessage}`);
102
+ }
103
+ }
104
+
105
+ private async handleToolCalls(
106
+ toolCalls: OpenAIToolCall[],
107
+ messages: OpenAIMessage[],
108
+ baseParams: Partial<OpenAIChatParams>,
109
+ modelToUse: string
110
+ ): Promise<EnhancedChatResponse> {
111
+ const pendingApprovals: { id: string; message: string }[] = [];
112
+ const toolMessages: OpenAIMessage[] = [];
113
+ let hasExecutedTools = false;
114
+
115
+ // Add the assistant message with tool calls
116
+ const assistantMessage: OpenAIMessage = {
117
+ role: 'assistant' as const,
118
+ content: null,
119
+ tool_calls: toolCalls,
120
+ };
121
+ messages.push(assistantMessage);
122
+
123
+ // Process each tool call
124
+ for (const toolCall of toolCalls) {
125
+ try {
126
+ const functionName = toolCall.function.name;
127
+ const args = JSON.parse(toolCall.function.arguments) as Record<string, unknown>;
128
+
129
+ logger.info(`${this.nickname} wants to call function: ${functionName}`);
130
+ SafeLogger.debug(`Function call arguments for ${functionName}:`, args);
131
+
132
+ const result = await this.functionBridge.handleFunctionCall(
133
+ this.nickname,
134
+ functionName,
135
+ args
136
+ );
137
+
138
+ if (result.needsApproval && result.approvalId) {
139
+ // Function needs approval
140
+ pendingApprovals.push({
141
+ id: result.approvalId,
142
+ message: result.message || `Approval needed for ${functionName}`,
143
+ });
144
+
145
+ // Add a tool message indicating approval is needed
146
+ toolMessages.push({
147
+ role: 'tool',
148
+ tool_call_id: toolCall.id,
149
+ content: JSON.stringify({
150
+ status: 'approval_needed',
151
+ approval_id: result.approvalId,
152
+ message: result.message,
153
+ }),
154
+ });
155
+
156
+ } else if (result.success && result.data) {
157
+ // Function executed successfully
158
+ hasExecutedTools = true;
159
+ toolMessages.push({
160
+ role: 'tool',
161
+ tool_call_id: toolCall.id,
162
+ content: typeof result.data === 'string'
163
+ ? result.data
164
+ : JSON.stringify(result.data),
165
+ });
166
+
167
+ } else {
168
+ // Function failed
169
+ toolMessages.push({
170
+ role: 'tool',
171
+ tool_call_id: toolCall.id,
172
+ content: JSON.stringify({
173
+ error: result.error || 'Unknown error',
174
+ }),
175
+ });
176
+ }
177
+
178
+ } catch (error: unknown) {
179
+ logger.error(`Error processing tool call ${toolCall.id}:`, error);
180
+ const errorMessage = error instanceof Error ? error.message : String(error);
181
+ toolMessages.push({
182
+ role: 'tool',
183
+ tool_call_id: toolCall.id,
184
+ content: JSON.stringify({
185
+ error: `Tool execution failed: ${errorMessage}`,
186
+ }),
187
+ });
188
+ }
189
+ }
190
+
191
+ // If we have pending approvals, return them without calling the model again
192
+ if (pendingApprovals.length > 0) {
193
+ const approvalMessage = pendingApprovals.length === 1
194
+ ? pendingApprovals[0].message
195
+ : `Multiple approvals needed: ${pendingApprovals.map(a => a.id).join(', ')}`;
196
+
197
+ return {
198
+ content: `⏳ ${approvalMessage}`,
199
+ model: modelToUse,
200
+ pendingApprovals,
201
+ finishReason: 'tool_calls',
202
+ };
203
+ }
204
+
205
+ // Add tool messages and call model again for final response
206
+ messages.push(...toolMessages);
207
+
208
+ // Remove tools from the follow-up call to get a natural language response
209
+ const followUpParams = {
210
+ ...baseParams,
211
+ messages,
212
+ };
213
+ delete followUpParams.tools;
214
+ delete followUpParams.tool_choice;
215
+
216
+ const finalResponse = await this.createChatCompletion(followUpParams);
217
+ const finalChoice = finalResponse.choices[0];
218
+
219
+ return {
220
+ content: finalChoice.message?.content || '',
221
+ usage: finalResponse.usage ? {
222
+ promptTokens: finalResponse.usage.prompt_tokens,
223
+ completionTokens: finalResponse.usage.completion_tokens,
224
+ totalTokens: finalResponse.usage.total_tokens,
225
+ } : undefined,
226
+ model: modelToUse,
227
+ finishReason: finalChoice.finish_reason || undefined,
228
+ mcpResults: hasExecutedTools ? (toolMessages as unknown as MCPResult[]) : undefined,
229
+ };
230
+ }
231
+
232
+ // Method to retry with approval
233
+ async retryWithApproval(
234
+ approvalId: string,
235
+ originalMessages: ConversationMessage[],
236
+ options: ChatOptions
237
+ ): Promise<EnhancedChatResponse> {
238
+ // Add approval ID to the tool arguments
239
+ if (options.tools) {
240
+ options.tools = options.tools.map(tool => ({
241
+ ...tool,
242
+ parameters: {
243
+ ...tool.parameters,
244
+ properties: {
245
+ ...(tool.parameters.properties as Record<string, unknown>),
246
+ _approval_id: {
247
+ type: 'string',
248
+ default: approvalId,
249
+ },
250
+ },
251
+ },
252
+ }));
253
+ }
254
+
255
+ // Retry the chat with the approval ID
256
+ return this.chat({
257
+ ...options,
258
+ messages: originalMessages,
259
+ });
260
+ }
261
+
262
+ // Check if MCP functions are available
263
+ async getMCPFunctionCount(): Promise<number> {
264
+ if (!this.mcpEnabled) {
265
+ return 0;
266
+ }
267
+
268
+ try {
269
+ const functions = await this.functionBridge.getFunctionDefinitions();
270
+ return functions.length;
271
+ } catch (error) {
272
+ return 0;
273
+ }
274
+ }
275
+
276
+ // Get MCP bridge statistics
277
+ getMCPStats() {
278
+ if (!this.mcpEnabled) {
279
+ return null;
280
+ }
281
+
282
+ return this.functionBridge.getStats();
283
+ }
284
+
285
+ // Enable/disable MCP for this provider
286
+ setMCPEnabled(enabled: boolean): void {
287
+ this.mcpEnabled = enabled;
288
+ logger.info(`MCP ${enabled ? 'enabled' : 'disabled'} for ${this.nickname}`);
289
+ }
290
+
291
+ isMCPEnabled(): boolean {
292
+ return this.mcpEnabled;
293
+ }
294
+ }
@@ -0,0 +1,290 @@
1
+ import { EnhancedDuckProvider } from './duck-provider-enhanced.js';
2
+ import { ProviderManager } from './manager.js';
3
+ import { ConfigManager } from '../config/config.js';
4
+ import { FunctionBridge } from '../services/function-bridge.js';
5
+ import { DuckResponse } from '../config/types.js';
6
+ import { ChatOptions, MCPResult } from './types.js';
7
+ import { logger } from '../utils/logger.js';
8
+
9
+ export class EnhancedProviderManager extends ProviderManager {
10
+ private enhancedProviders: Map<string, EnhancedDuckProvider> = new Map();
11
+ private functionBridge?: FunctionBridge;
12
+ private mcpEnabled: boolean = false;
13
+
14
+ constructor(configManager: ConfigManager, functionBridge?: FunctionBridge) {
15
+ super(configManager);
16
+ this.functionBridge = functionBridge;
17
+ this.mcpEnabled = !!functionBridge &&
18
+ (configManager.getConfig().mcp_bridge?.enabled || false);
19
+
20
+ if (this.mcpEnabled) {
21
+ this.initializeEnhancedProviders();
22
+ }
23
+ }
24
+
25
+ private initializeEnhancedProviders() {
26
+ if (!this.functionBridge) {
27
+ logger.warn('Function bridge not available, skipping enhanced providers');
28
+ return;
29
+ }
30
+
31
+ const config = this.configManager.getConfig();
32
+ const allProviders = config.providers;
33
+
34
+ for (const [name, providerConfig] of Object.entries(allProviders)) {
35
+ try {
36
+ // Create enhanced provider if MCP is enabled
37
+ const enhancedProvider = new EnhancedDuckProvider(
38
+ name,
39
+ providerConfig.nickname,
40
+ {
41
+ apiKey: providerConfig.api_key,
42
+ baseURL: providerConfig.base_url,
43
+ model: providerConfig.default_model,
44
+ availableModels: providerConfig.models,
45
+ temperature: providerConfig.temperature,
46
+ timeout: providerConfig.timeout,
47
+ maxRetries: providerConfig.max_retries,
48
+ systemPrompt: providerConfig.system_prompt,
49
+ },
50
+ this.functionBridge,
51
+ this.mcpEnabled
52
+ );
53
+
54
+ this.enhancedProviders.set(name, enhancedProvider);
55
+ logger.info(`Initialized enhanced provider: ${name} (${providerConfig.nickname}) with MCP support`);
56
+ } catch (error) {
57
+ logger.error(`Failed to initialize enhanced provider ${name}:`, error);
58
+ }
59
+ }
60
+ }
61
+
62
+ getEnhancedProvider(name?: string): EnhancedDuckProvider {
63
+ if (!this.mcpEnabled) {
64
+ throw new Error('MCP bridge is not enabled');
65
+ }
66
+
67
+ const providerName = name || this.configManager.getConfig().default_provider;
68
+
69
+ if (!providerName) {
70
+ throw new Error('No provider specified and no default provider configured');
71
+ }
72
+
73
+ const provider = this.enhancedProviders.get(providerName);
74
+
75
+ if (!provider) {
76
+ throw new Error(`Enhanced duck "${providerName}" not found in the pond`);
77
+ }
78
+
79
+ return provider;
80
+ }
81
+
82
+ async askDuckWithMCP(
83
+ providerName: string | undefined,
84
+ prompt: string,
85
+ options?: Partial<ChatOptions>
86
+ ): Promise<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }> {
87
+ if (!this.mcpEnabled) {
88
+ // Fall back to regular provider
89
+ return this.askDuck(providerName, prompt, options);
90
+ }
91
+
92
+ const provider = this.getEnhancedProvider(providerName);
93
+ const startTime = Date.now();
94
+
95
+ try {
96
+ const response = await provider.chat({
97
+ messages: [{ role: 'user', content: prompt, timestamp: new Date() }],
98
+ ...options,
99
+ });
100
+
101
+ return {
102
+ provider: provider.name,
103
+ nickname: provider.nickname,
104
+ model: response.model,
105
+ content: response.content,
106
+ usage: response.usage ? {
107
+ prompt_tokens: response.usage.promptTokens,
108
+ completion_tokens: response.usage.completionTokens,
109
+ total_tokens: response.usage.totalTokens,
110
+ promptTokens: response.usage.promptTokens,
111
+ completionTokens: response.usage.completionTokens,
112
+ totalTokens: response.usage.totalTokens,
113
+ } : undefined,
114
+ latency: Date.now() - startTime,
115
+ cached: false,
116
+ pendingApprovals: response.pendingApprovals,
117
+ mcpResults: response.mcpResults,
118
+ };
119
+ } catch (error: unknown) {
120
+ // Try failover if enabled
121
+ if (this.configManager.getConfig().enable_failover && providerName === undefined) {
122
+ const errorMessage = error instanceof Error ? error.message : String(error);
123
+ logger.warn(`Primary enhanced provider failed, attempting failover: ${errorMessage}`);
124
+ return this.askDuckWithMCPFailover(prompt, options, provider.name);
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ private async askDuckWithMCPFailover(
131
+ prompt: string,
132
+ options: Partial<ChatOptions> | undefined,
133
+ failedProvider: string
134
+ ): Promise<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }> {
135
+ const availableProviders = Array.from(this.enhancedProviders.keys()).filter(
136
+ name => name !== failedProvider
137
+ );
138
+
139
+ for (const providerName of availableProviders) {
140
+ try {
141
+ logger.info(`Trying enhanced failover to ${providerName}...`);
142
+ return await this.askDuckWithMCP(providerName, prompt, options);
143
+ } catch (error) {
144
+ logger.warn(`Enhanced failover to ${providerName} failed:`, error);
145
+ continue;
146
+ }
147
+ }
148
+
149
+ throw new Error('All enhanced ducks have flown away! No providers available.');
150
+ }
151
+
152
+ async compareDucksWithMCP(
153
+ prompt: string,
154
+ providerNames?: string[],
155
+ options?: Partial<ChatOptions>
156
+ ): Promise<Array<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }>> {
157
+ if (!this.mcpEnabled) {
158
+ // Fall back to regular comparison
159
+ return this.compareDucks(prompt, providerNames, options);
160
+ }
161
+
162
+ const providersToUse = providerNames
163
+ ? providerNames.map(name => this.enhancedProviders.get(name)).filter(Boolean)
164
+ : Array.from(this.enhancedProviders.values());
165
+
166
+ if (providersToUse.length === 0) {
167
+ throw new Error('No valid enhanced providers specified');
168
+ }
169
+
170
+ const promises = providersToUse.map(provider =>
171
+ provider ? this.askDuckWithMCP(provider.name, prompt, options).catch(error => ({
172
+ provider: provider.name,
173
+ nickname: provider.nickname,
174
+ model: '',
175
+ content: `Error: ${error instanceof Error ? error.message : String(error)}`,
176
+ latency: 0,
177
+ cached: false,
178
+ })) : Promise.resolve({
179
+ provider: 'unknown',
180
+ nickname: 'Unknown',
181
+ model: '',
182
+ content: 'Error: Invalid provider',
183
+ latency: 0,
184
+ cached: false,
185
+ })
186
+ );
187
+
188
+ return Promise.all(promises);
189
+ }
190
+
191
+ async duckCouncilWithMCP(
192
+ prompt: string,
193
+ options?: Partial<ChatOptions>
194
+ ): Promise<Array<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }>> {
195
+ return this.compareDucksWithMCP(prompt, undefined, options);
196
+ }
197
+
198
+ // Method to retry with approval
199
+ async retryWithApproval(
200
+ approvalId: string,
201
+ providerName: string | undefined,
202
+ prompt: string,
203
+ options?: Partial<ChatOptions>
204
+ ): Promise<DuckResponse & { pendingApprovals?: { id: string; message: string }[]; mcpResults?: MCPResult[] }> {
205
+ if (!this.mcpEnabled) {
206
+ throw new Error('MCP bridge is not enabled');
207
+ }
208
+
209
+ const provider = this.getEnhancedProvider(providerName);
210
+ const startTime = Date.now();
211
+
212
+ try {
213
+ const response = await provider.retryWithApproval(
214
+ approvalId,
215
+ [{ role: 'user', content: prompt, timestamp: new Date() }],
216
+ {
217
+ messages: [{ role: 'user', content: prompt, timestamp: new Date() }],
218
+ ...options,
219
+ }
220
+ );
221
+
222
+ return {
223
+ provider: provider.name,
224
+ nickname: provider.nickname,
225
+ model: response.model,
226
+ content: response.content,
227
+ usage: response.usage ? {
228
+ prompt_tokens: response.usage.promptTokens,
229
+ completion_tokens: response.usage.completionTokens,
230
+ total_tokens: response.usage.totalTokens,
231
+ promptTokens: response.usage.promptTokens,
232
+ completionTokens: response.usage.completionTokens,
233
+ totalTokens: response.usage.totalTokens,
234
+ } : undefined,
235
+ latency: Date.now() - startTime,
236
+ cached: false,
237
+ pendingApprovals: response.pendingApprovals,
238
+ mcpResults: response.mcpResults,
239
+ };
240
+ } catch (error: unknown) {
241
+ const errorMessage = error instanceof Error ? error.message : String(error);
242
+ throw new Error(`Failed to retry with approval: ${errorMessage}`);
243
+ }
244
+ }
245
+
246
+ // Get enhanced provider statistics
247
+ getAllEnhancedProviders(): Array<{
248
+ name: string;
249
+ info: ReturnType<EnhancedDuckProvider['getInfo']>;
250
+ mcpEnabled: boolean;
251
+ mcpStats?: ReturnType<EnhancedDuckProvider['getMCPStats']>;
252
+ functionCount?: number;
253
+ }> {
254
+ return Array.from(this.enhancedProviders.entries()).map(([name, provider]) => ({
255
+ name,
256
+ info: provider.getInfo(),
257
+ mcpEnabled: provider.isMCPEnabled(),
258
+ mcpStats: provider.getMCPStats(),
259
+ functionCount: 0, // Will be populated when functions are loaded
260
+ }));
261
+ }
262
+
263
+ // Check if MCP is enabled
264
+ isMCPEnabled(): boolean {
265
+ return this.mcpEnabled;
266
+ }
267
+
268
+ // Enable/disable MCP for all providers
269
+ setMCPEnabled(enabled: boolean): void {
270
+ this.mcpEnabled = enabled;
271
+ for (const provider of this.enhancedProviders.values()) {
272
+ provider.setMCPEnabled(enabled);
273
+ }
274
+ logger.info(`MCP ${enabled ? 'enabled' : 'disabled'} for all providers`);
275
+ }
276
+
277
+ // Get MCP function count for a provider
278
+ async getMCPFunctionCount(providerName?: string): Promise<number> {
279
+ if (!this.mcpEnabled) {
280
+ return 0;
281
+ }
282
+
283
+ try {
284
+ const provider = this.getEnhancedProvider(providerName);
285
+ return await provider.getMCPFunctionCount();
286
+ } catch (error) {
287
+ return 0;
288
+ }
289
+ }
290
+ }