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,362 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
+ import { logger } from '../utils/logger.js';
5
+ import { SafeLogger } from '../utils/safe-logger.js';
6
+
7
+ export interface MCPServerConfig {
8
+ name: string;
9
+ type: 'stdio' | 'http';
10
+ command?: string; // for stdio
11
+ args?: string[]; // for stdio
12
+ url?: string; // for http
13
+ apiKey?: string; // for http auth
14
+ enabled?: boolean;
15
+ retryAttempts?: number; // Number of retry attempts (default: 3)
16
+ retryDelay?: number; // Initial retry delay in ms (default: 1000)
17
+ }
18
+
19
+ export interface MCPTool {
20
+ serverName: string;
21
+ name: string;
22
+ description: string;
23
+ inputSchema: Record<string, unknown>;
24
+ }
25
+
26
+ export class MCPClientManager {
27
+ private clients: Map<string, Client> = new Map();
28
+ private configs: MCPServerConfig[] = [];
29
+ private connectionStatus: Map<string, 'connected' | 'connecting' | 'disconnected' | 'error'> = new Map();
30
+ private retryInfo: Map<string, { attempts: number; lastAttempt: number }> = new Map();
31
+
32
+ constructor(configs: MCPServerConfig[] = []) {
33
+ this.configs = configs.filter(config => config.enabled !== false);
34
+ }
35
+
36
+ async initialize(): Promise<void> {
37
+ logger.info(`Initializing MCP Client Manager with ${this.configs.length} servers`);
38
+
39
+ const connectionPromises = this.configs.map(config => this.connectToServer(config));
40
+
41
+ // Connect to all servers in parallel, but don't fail if some fail
42
+ const results = await Promise.allSettled(connectionPromises);
43
+
44
+ let successCount = 0;
45
+ results.forEach((result, index) => {
46
+ const serverName = this.configs[index].name;
47
+ if (result.status === 'fulfilled') {
48
+ successCount++;
49
+ logger.info(`Successfully connected to MCP server: ${serverName}`);
50
+ } else {
51
+ logger.error(`Failed to connect to MCP server ${serverName}:`, result.reason);
52
+ this.connectionStatus.set(serverName, 'error');
53
+ }
54
+ });
55
+
56
+ logger.info(`MCP Client Manager initialized: ${successCount}/${this.configs.length} servers connected`);
57
+ }
58
+
59
+ private async connectToServer(config: MCPServerConfig): Promise<Client> {
60
+ const { name, type } = config;
61
+ const maxRetries = config.retryAttempts ?? 3;
62
+ const initialDelay = config.retryDelay ?? 1000;
63
+
64
+ if (this.clients.has(name)) {
65
+ logger.warn(`MCP server ${name} already connected`);
66
+ return this.clients.get(name)!;
67
+ }
68
+
69
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
70
+ try {
71
+ this.connectionStatus.set(name, 'connecting');
72
+ this.retryInfo.set(name, { attempts: attempt, lastAttempt: Date.now() });
73
+
74
+ if (attempt > 0) {
75
+ const delay = initialDelay * Math.pow(2, attempt - 1); // Exponential backoff
76
+ logger.info(`Retrying connection to MCP server ${name} (attempt ${attempt}/${maxRetries}) after ${delay}ms`);
77
+ await this.sleep(delay);
78
+ }
79
+
80
+ // Create client
81
+ const client = new Client({
82
+ name: 'mcp-rubber-duck',
83
+ version: '1.0.0',
84
+ }, {
85
+ capabilities: {
86
+ tools: {},
87
+ },
88
+ });
89
+
90
+ // Create transport based on type
91
+ let transport;
92
+ if (type === 'stdio') {
93
+ if (!config.command) {
94
+ throw new Error(`stdio server ${name} requires command`);
95
+ }
96
+
97
+ transport = new StdioClientTransport({
98
+ command: config.command,
99
+ args: config.args || [],
100
+ });
101
+ } else if (type === 'http') {
102
+ if (!config.url) {
103
+ throw new Error(`http server ${name} requires url`);
104
+ }
105
+
106
+ // HTTP transport using StreamableHTTPClientTransport
107
+ const url = new URL(config.url);
108
+ const transportOptions: { requestInit?: { headers?: Record<string, string> } } = {};
109
+
110
+ if (config.apiKey) {
111
+ transportOptions.requestInit = {
112
+ headers: {
113
+ 'Authorization': `Bearer ${config.apiKey}`,
114
+ },
115
+ };
116
+ }
117
+
118
+ transport = new StreamableHTTPClientTransport(url, transportOptions);
119
+ } else {
120
+ throw new Error(`Unsupported transport type: ${String(type)}`);
121
+ }
122
+
123
+ // Connect to server with timeout
124
+ const connectTimeout = new Promise((_, reject) =>
125
+ setTimeout(() => reject(new Error('Connection timeout')), 30000)
126
+ );
127
+
128
+ await Promise.race([
129
+ client.connect(transport),
130
+ connectTimeout
131
+ ]);
132
+
133
+ this.clients.set(name, client);
134
+ this.connectionStatus.set(name, 'connected');
135
+ this.retryInfo.delete(name); // Clear retry info on success
136
+
137
+ logger.info(`Connected to MCP server: ${name} (${type}) after ${attempt} retries`);
138
+ return client;
139
+
140
+ } catch (error: unknown) {
141
+ logger.warn(`Failed to connect to MCP server ${name} (attempt ${attempt + 1}/${maxRetries + 1}):`, error instanceof Error ? error.message : String(error));
142
+
143
+ if (attempt === maxRetries) {
144
+ this.connectionStatus.set(name, 'error');
145
+ this.retryInfo.set(name, { attempts: attempt + 1, lastAttempt: Date.now() });
146
+ logger.error(`All retry attempts exhausted for MCP server ${name}`);
147
+ throw error;
148
+ }
149
+ }
150
+ }
151
+
152
+ throw new Error(`Failed to connect to MCP server ${name} after ${maxRetries + 1} attempts`);
153
+ }
154
+
155
+ private sleep(ms: number): Promise<void> {
156
+ return new Promise(resolve => setTimeout(resolve, ms));
157
+ }
158
+
159
+ async disconnectAll(): Promise<void> {
160
+ logger.info('Disconnecting all MCP clients');
161
+
162
+ const disconnectPromises = Array.from(this.clients.entries()).map(async ([name, client]) => {
163
+ try {
164
+ await client.close();
165
+ this.connectionStatus.set(name, 'disconnected');
166
+ logger.info(`Disconnected from MCP server: ${name}`);
167
+ } catch (error: unknown) {
168
+ const errorMessage = error instanceof Error ? error.message : String(error);
169
+ logger.error(`Error disconnecting from MCP server ${name}:`, errorMessage);
170
+ }
171
+ });
172
+
173
+ await Promise.allSettled(disconnectPromises);
174
+
175
+ this.clients.clear();
176
+ logger.info('All MCP clients disconnected');
177
+ }
178
+
179
+ getClient(serverName: string): Client | undefined {
180
+ const client = this.clients.get(serverName);
181
+ const status = this.connectionStatus.get(serverName);
182
+
183
+ if (!client || status !== 'connected') {
184
+ return undefined;
185
+ }
186
+
187
+ return client;
188
+ }
189
+
190
+ getConnectionStatus(serverName: string): 'connected' | 'connecting' | 'disconnected' | 'error' | 'unknown' {
191
+ return this.connectionStatus.get(serverName) || 'unknown';
192
+ }
193
+
194
+ getConnectedServers(): string[] {
195
+ return Array.from(this.clients.keys()).filter(name =>
196
+ this.connectionStatus.get(name) === 'connected'
197
+ );
198
+ }
199
+
200
+ async listAllTools(): Promise<MCPTool[]> {
201
+ const allTools: MCPTool[] = [];
202
+
203
+ for (const [serverName, client] of this.clients.entries()) {
204
+ if (this.connectionStatus.get(serverName) !== 'connected') {
205
+ continue;
206
+ }
207
+
208
+ try {
209
+ const toolsResult = await client.listTools();
210
+
211
+ if (toolsResult.tools) {
212
+ const serverTools = toolsResult.tools.map(tool => ({
213
+ serverName,
214
+ name: tool.name,
215
+ description: tool.description || '',
216
+ inputSchema: tool.inputSchema,
217
+ }));
218
+
219
+ allTools.push(...serverTools);
220
+ logger.debug(`Listed ${serverTools.length} tools from ${serverName}`);
221
+ }
222
+ } catch (error: unknown) {
223
+ const errorMessage = error instanceof Error ? error.message : String(error);
224
+ logger.error(`Failed to list tools from ${serverName}:`, errorMessage);
225
+ }
226
+ }
227
+
228
+ logger.debug(`Total MCP tools available: ${allTools.length}`);
229
+ return allTools;
230
+ }
231
+
232
+ async listServerTools(serverName: string): Promise<MCPTool[]> {
233
+ const client = this.getClient(serverName);
234
+ if (!client) {
235
+ throw new Error(`MCP server ${serverName} not connected`);
236
+ }
237
+
238
+ try {
239
+ const toolsResult = await client.listTools();
240
+
241
+ if (!toolsResult.tools) {
242
+ return [];
243
+ }
244
+
245
+ return toolsResult.tools.map(tool => ({
246
+ serverName,
247
+ name: tool.name,
248
+ description: tool.description || '',
249
+ inputSchema: tool.inputSchema,
250
+ }));
251
+ } catch (error: unknown) {
252
+ logger.error(`Failed to list tools from ${serverName}:`, error instanceof Error ? error.message : String(error));
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ async callTool(serverName: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
258
+ const client = this.getClient(serverName);
259
+ if (!client) {
260
+ throw new Error(`MCP server ${serverName} not connected`);
261
+ }
262
+
263
+ try {
264
+ SafeLogger.debug(`Calling MCP tool ${serverName}:${toolName} with args:`, args);
265
+
266
+ const result = await client.callTool({
267
+ name: toolName,
268
+ arguments: args,
269
+ });
270
+
271
+ SafeLogger.debug(`MCP tool ${serverName}:${toolName} returned:`, result);
272
+ return result;
273
+
274
+ } catch (error: unknown) {
275
+ logger.error(`Failed to call MCP tool ${serverName}:${toolName}:`, error instanceof Error ? error.message : String(error));
276
+ throw error;
277
+ }
278
+ }
279
+
280
+ // Health check for all connected servers
281
+ async healthCheck(): Promise<Record<string, boolean>> {
282
+ const healthStatus: Record<string, boolean> = {};
283
+
284
+ for (const [serverName, client] of this.clients.entries()) {
285
+ try {
286
+ // Try to list tools as a health check
287
+ await client.listTools();
288
+ healthStatus[serverName] = true;
289
+ this.connectionStatus.set(serverName, 'connected');
290
+ } catch (error: unknown) {
291
+ healthStatus[serverName] = false;
292
+ this.connectionStatus.set(serverName, 'error');
293
+ logger.warn(`Health check failed for MCP server ${serverName}:`, error instanceof Error ? error.message : String(error));
294
+ }
295
+ }
296
+
297
+ return healthStatus;
298
+ }
299
+
300
+ // Get status of all servers
301
+ getStatus(): Record<string, {
302
+ status: string;
303
+ type: string;
304
+ toolCount?: number;
305
+ retryAttempts?: number;
306
+ lastAttempt?: number;
307
+ }> {
308
+ const status: Record<string, {
309
+ status: string;
310
+ type: string;
311
+ retryAttempts?: number;
312
+ lastAttempt?: number;
313
+ }> = {};
314
+
315
+ this.configs.forEach(config => {
316
+ const retryInfo = this.retryInfo.get(config.name);
317
+ status[config.name] = {
318
+ status: this.connectionStatus.get(config.name) || 'unknown',
319
+ type: config.type,
320
+ ...(retryInfo && {
321
+ retryAttempts: retryInfo.attempts,
322
+ lastAttempt: retryInfo.lastAttempt,
323
+ }),
324
+ };
325
+ });
326
+
327
+ return status;
328
+ }
329
+
330
+ // Get retry information for a specific server
331
+ getRetryInfo(serverName: string): { attempts: number; lastAttempt: number } | undefined {
332
+ return this.retryInfo.get(serverName);
333
+ }
334
+
335
+ // Retry connection for a specific server
336
+ async retryConnection(serverName: string): Promise<boolean> {
337
+ const config = this.configs.find(c => c.name === serverName);
338
+ if (!config) {
339
+ logger.error(`Server config not found for ${serverName}`);
340
+ return false;
341
+ }
342
+
343
+ try {
344
+ // Disconnect first if connected
345
+ const existingClient = this.clients.get(serverName);
346
+ if (existingClient) {
347
+ await existingClient.close();
348
+ this.clients.delete(serverName);
349
+ }
350
+
351
+ // Reset retry info
352
+ this.retryInfo.delete(serverName);
353
+
354
+ // Attempt to connect
355
+ await this.connectToServer(config);
356
+ return true;
357
+ } catch (error: unknown) {
358
+ logger.error(`Manual retry failed for ${serverName}:`, error instanceof Error ? error.message : String(error));
359
+ return false;
360
+ }
361
+ }
362
+ }
@@ -0,0 +1,126 @@
1
+ import { ApprovalService } from '../services/approval.js';
2
+
3
+ export function approveMCPRequestTool(
4
+ approvalService: ApprovalService,
5
+ args: Record<string, unknown>
6
+ ) {
7
+ const { approval_id, decision, reason } = args as {
8
+ approval_id?: string;
9
+ decision?: string;
10
+ reason?: string;
11
+ };
12
+
13
+ if (!approval_id || !decision) {
14
+ return {
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: '❌ Missing required parameters: approval_id and decision are required',
19
+ },
20
+ ],
21
+ isError: true,
22
+ };
23
+ }
24
+
25
+ if (!['approve', 'deny'].includes(decision)) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: 'text',
30
+ text: '❌ Decision must be either "approve" or "deny"',
31
+ },
32
+ ],
33
+ isError: true,
34
+ };
35
+ }
36
+
37
+ try {
38
+ // Get the request details before processing
39
+ const request = approvalService.getApprovalRequest(approval_id);
40
+
41
+ if (!request) {
42
+ return {
43
+ content: [
44
+ {
45
+ type: 'text',
46
+ text: `❌ Approval request ${approval_id} not found`,
47
+ },
48
+ ],
49
+ isError: true,
50
+ };
51
+ }
52
+
53
+ if (request.status !== 'pending') {
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ text: `❌ Request ${approval_id} is not pending (status: ${request.status})`,
59
+ },
60
+ ],
61
+ isError: true,
62
+ };
63
+ }
64
+
65
+ let success = false;
66
+ let message = '';
67
+
68
+ if (decision === 'approve') {
69
+ success = approvalService.approveRequest(approval_id);
70
+ if (success) {
71
+ message = `✅ Approved: ${request.duckName} can now call ${request.mcpServer}:${request.toolName}`;
72
+ } else {
73
+ message = `❌ Failed to approve request ${approval_id}`;
74
+ }
75
+ } else {
76
+ success = approvalService.denyRequest(approval_id, reason);
77
+ if (success) {
78
+ message = `❌ Denied: ${request.duckName} cannot call ${request.mcpServer}:${request.toolName}`;
79
+ if (reason) {
80
+ message += `\nReason: ${reason}`;
81
+ }
82
+ } else {
83
+ message = `❌ Failed to deny request ${approval_id}`;
84
+ }
85
+ }
86
+
87
+ // Add request details for context
88
+ const detailsLines = [
89
+ message,
90
+ '',
91
+ '📋 **Request Details:**',
92
+ `- Duck: ${request.duckName}`,
93
+ `- MCP Server: ${request.mcpServer}`,
94
+ `- Tool: ${request.toolName}`,
95
+ `- Arguments: \`${JSON.stringify(request.arguments)}\``,
96
+ `- Request ID: \`${request.id}\``,
97
+ ];
98
+
99
+ // Add next steps
100
+ if (decision === 'approve') {
101
+ detailsLines.push('');
102
+ detailsLines.push('💡 The duck can now retry their request with the approval.');
103
+ }
104
+
105
+ return {
106
+ content: [
107
+ {
108
+ type: 'text',
109
+ text: detailsLines.join('\n'),
110
+ },
111
+ ],
112
+ isError: !success,
113
+ };
114
+
115
+ } catch (error: unknown) {
116
+ return {
117
+ content: [
118
+ {
119
+ type: 'text',
120
+ text: `❌ Error processing approval: ${error instanceof Error ? error.message : String(error)}`,
121
+ },
122
+ ],
123
+ isError: true,
124
+ };
125
+ }
126
+ }
@@ -0,0 +1,74 @@
1
+ import { ProviderManager } from '../providers/manager.js';
2
+ import { ResponseCache } from '../services/cache.js';
3
+ import { formatDuckResponse } from '../utils/ascii-art.js';
4
+ import { logger } from '../utils/logger.js';
5
+
6
+ export async function askDuckTool(
7
+ providerManager: ProviderManager,
8
+ cache: ResponseCache,
9
+ args: Record<string, unknown>
10
+ ) {
11
+ const { prompt, provider, model, temperature } = args as {
12
+ prompt?: string;
13
+ provider?: string;
14
+ model?: string;
15
+ temperature?: number;
16
+ };
17
+
18
+ if (!prompt) {
19
+ throw new Error('Prompt is required');
20
+ }
21
+
22
+ // Validate model if provided
23
+ if (model && provider) {
24
+ const isValid = providerManager.validateModel(provider, model);
25
+ if (!isValid) {
26
+ logger.warn(`Model ${model} may not be valid for provider ${provider}`);
27
+ }
28
+ }
29
+
30
+ // Generate cache key
31
+ const cacheKey = cache.generateKey(
32
+ provider || 'default',
33
+ prompt,
34
+ { model, temperature }
35
+ );
36
+
37
+ // Try to get cached response
38
+ const { value: response, cached } = await cache.getOrSet(
39
+ cacheKey,
40
+ async () => {
41
+ return await providerManager.askDuck(provider, prompt, {
42
+ model,
43
+ temperature,
44
+ });
45
+ }
46
+ );
47
+
48
+ // Format the response
49
+ const formattedResponse = formatDuckResponse(
50
+ response.nickname,
51
+ response.content,
52
+ response.model
53
+ );
54
+
55
+ // Add usage info if available
56
+ let usageInfo = '';
57
+ if (response.usage) {
58
+ usageInfo = `\n\n📊 Tokens used: ${response.usage.total_tokens} (${response.usage.prompt_tokens} prompt, ${response.usage.completion_tokens} completion)`;
59
+ }
60
+
61
+ // Add cache and latency info
62
+ const metaInfo = `\n⏱️ Latency: ${response.latency}ms | ${cached ? '💾 Cached' : '🔄 Fresh'}`;
63
+
64
+ logger.info(`Duck ${response.nickname} responded to query ${cached ? '(cached)' : ''}`);
65
+
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: formattedResponse + usageInfo + metaInfo,
71
+ },
72
+ ],
73
+ };
74
+ }
@@ -0,0 +1,82 @@
1
+ import { ProviderManager } from '../providers/manager.js';
2
+ import { ConversationManager } from '../services/conversation.js';
3
+ import { formatDuckResponse } from '../utils/ascii-art.js';
4
+ import { logger } from '../utils/logger.js';
5
+
6
+ export async function chatDuckTool(
7
+ providerManager: ProviderManager,
8
+ conversationManager: ConversationManager,
9
+ args: Record<string, unknown>
10
+ ) {
11
+ const { conversation_id, message, provider, model } = args as {
12
+ conversation_id?: string;
13
+ message?: string;
14
+ provider?: string;
15
+ model?: string;
16
+ };
17
+
18
+ if (!conversation_id || !message) {
19
+ throw new Error('conversation_id and message are required');
20
+ }
21
+
22
+ // Get or create conversation
23
+ let conversation = conversationManager.getConversation(conversation_id);
24
+
25
+ if (!conversation) {
26
+ // Create new conversation with specified or default provider
27
+ const providerName = provider || providerManager.getProviderNames()[0];
28
+ conversation = conversationManager.createConversation(conversation_id, providerName);
29
+ logger.info(`Created new conversation: ${conversation_id} with ${providerName}`);
30
+ } else if (provider && provider !== conversation.provider) {
31
+ // Switch provider if requested
32
+ conversation = conversationManager.switchProvider(conversation_id, provider);
33
+ logger.info(`Switched conversation ${conversation_id} to ${provider}`);
34
+ }
35
+
36
+ // Add user message to conversation
37
+ conversationManager.addMessage(conversation_id, {
38
+ role: 'user',
39
+ content: message,
40
+ timestamp: new Date(),
41
+ });
42
+
43
+ // Get conversation context
44
+ const messages = conversationManager.getConversationContext(conversation_id);
45
+
46
+ // Get response from provider
47
+ const providerToUse = provider || conversation.provider;
48
+ const response = await providerManager.askDuck(providerToUse, '', {
49
+ messages,
50
+ model,
51
+ });
52
+
53
+ // Add assistant response to conversation
54
+ conversationManager.addMessage(conversation_id, {
55
+ role: 'assistant',
56
+ content: response.content,
57
+ timestamp: new Date(),
58
+ provider: providerToUse,
59
+ });
60
+
61
+ // Format response
62
+ const formattedResponse = formatDuckResponse(
63
+ response.nickname,
64
+ response.content,
65
+ response.model
66
+ );
67
+
68
+ // Add conversation info
69
+ const conversationInfo = `\n\n💬 Conversation: ${conversation_id} | Messages: ${messages.length + 1}`;
70
+ const latencyInfo = `\n⏱️ Latency: ${response.latency}ms`;
71
+
72
+ logger.info(`Duck ${response.nickname} responded in conversation ${conversation_id}`);
73
+
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: formattedResponse + conversationInfo + latencyInfo,
79
+ },
80
+ ],
81
+ };
82
+ }
@@ -0,0 +1,24 @@
1
+ import { ConversationManager } from '../services/conversation.js';
2
+ import { logger } from '../utils/logger.js';
3
+
4
+ export function clearConversationsTool(
5
+ conversationManager: ConversationManager,
6
+ _args: Record<string, unknown>
7
+ ) {
8
+ const result = conversationManager.clearAll();
9
+
10
+ logger.info(`User cleared ${result.conversationsCleared} conversations`);
11
+
12
+ const message = result.conversationsCleared === 0
13
+ ? '🧹 No conversations to clear - memory is already empty!'
14
+ : `🧹 Cleared ${result.conversationsCleared} conversation${result.conversationsCleared === 1 ? '' : 's'} (${result.messagesCleared} message${result.messagesCleared === 1 ? '' : 's'})`;
15
+
16
+ return {
17
+ content: [
18
+ {
19
+ type: 'text',
20
+ text: `${message}\n\n🦆 All ducks now have a fresh start! Previous conversation context has been removed.`,
21
+ },
22
+ ],
23
+ };
24
+ }