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,329 @@
1
+ import { MCPClientManager, MCPTool } from './mcp-client-manager.js';
2
+ import { ApprovalService } from './approval.js';
3
+ import { logger } from '../utils/logger.js';
4
+ import Ajv, { ValidateFunction } from 'ajv';
5
+
6
+ export interface FunctionDefinition {
7
+ name: string;
8
+ description: string;
9
+ parameters: Record<string, unknown>; // JSON Schema
10
+ }
11
+
12
+ export interface FunctionCallResult {
13
+ success: boolean;
14
+ needsApproval?: boolean;
15
+ approvalId?: string;
16
+ data?: unknown;
17
+ error?: string;
18
+ message?: string;
19
+ }
20
+
21
+ export class FunctionBridge {
22
+ private mcpManager: MCPClientManager;
23
+ private approvalService: ApprovalService;
24
+ private trustedTools: Set<string> = new Set();
25
+ private trustedToolsByServer: Map<string, Set<string>> = new Map();
26
+ private ajv: unknown;
27
+ private toolSchemas: Map<string, Record<string, unknown>> = new Map();
28
+ private approvalMode: 'always' | 'trusted' | 'never';
29
+
30
+ constructor(
31
+ mcpManager: MCPClientManager,
32
+ approvalService: ApprovalService,
33
+ trustedTools: string[] = [],
34
+ approvalMode: 'always' | 'trusted' | 'never' = 'always',
35
+ trustedToolsByServer: Record<string, string[]> = {}
36
+ ) {
37
+ this.mcpManager = mcpManager;
38
+ this.approvalService = approvalService;
39
+ this.trustedTools = new Set(trustedTools);
40
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
41
+ this.ajv = new (Ajv as unknown as new (options: unknown) => unknown)({ allErrors: true, removeAdditional: 'all' });
42
+ this.approvalMode = approvalMode;
43
+
44
+ // Initialize per-server trusted tools
45
+ Object.entries(trustedToolsByServer).forEach(([serverName, tools]) => {
46
+ this.trustedToolsByServer.set(serverName, new Set(tools));
47
+ });
48
+ }
49
+
50
+ async getFunctionDefinitions(): Promise<FunctionDefinition[]> {
51
+ try {
52
+ const mcpTools = await this.mcpManager.listAllTools();
53
+
54
+ const functionDefinitions: FunctionDefinition[] = mcpTools.map(tool => {
55
+ const functionDef = this.convertMCPToolToFunction(tool);
56
+ // Cache the tool schema for validation
57
+ const toolKey = `${tool.serverName}:${tool.name}`;
58
+ this.toolSchemas.set(toolKey, tool.inputSchema);
59
+ return functionDef;
60
+ });
61
+
62
+ logger.debug(`Generated ${functionDefinitions.length} function definitions from MCP tools`);
63
+ return functionDefinitions;
64
+
65
+ } catch (error: unknown) {
66
+ const errorMessage = error instanceof Error ? error.message : String(error);
67
+ logger.error('Failed to generate function definitions:', errorMessage);
68
+ return [];
69
+ }
70
+ }
71
+
72
+ private convertMCPToolToFunction(mcpTool: MCPTool): FunctionDefinition {
73
+ return {
74
+ name: `mcp__${mcpTool.serverName}__${mcpTool.name}`,
75
+ description: `[${mcpTool.serverName}] ${mcpTool.description}`,
76
+ parameters: {
77
+ type: 'object',
78
+ properties: {
79
+ // Include the MCP tool's original parameters
80
+ ...((mcpTool.inputSchema as { properties?: Record<string, unknown> })?.properties || {}),
81
+
82
+ // Add our internal parameters
83
+ _mcp_server: {
84
+ type: 'string',
85
+ description: 'Internal: MCP server name',
86
+ default: mcpTool.serverName,
87
+ },
88
+ _mcp_tool: {
89
+ type: 'string',
90
+ description: 'Internal: MCP tool name',
91
+ default: mcpTool.name,
92
+ },
93
+ _approval_id: {
94
+ type: 'string',
95
+ description: 'Internal: Approval ID if pre-approved',
96
+ },
97
+ },
98
+ required: ((mcpTool.inputSchema as { required?: string[] })?.required || []),
99
+ },
100
+ };
101
+ }
102
+
103
+ private validateToolArguments(toolKey: string, args: Record<string, unknown>): { valid: boolean; errors?: string[] } {
104
+ const schema = this.toolSchemas.get(toolKey);
105
+ if (!schema) {
106
+ return { valid: true }; // No schema available, skip validation
107
+ }
108
+
109
+ try {
110
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
111
+ const validate: ValidateFunction = (this.ajv as { compile: (schema: unknown) => ValidateFunction }).compile(schema);
112
+ const valid = validate(args);
113
+
114
+ if (!valid && validate.errors) {
115
+ const errors = validate.errors.map(err =>
116
+ `${err.instancePath || 'root'}: ${err.message || 'validation error'}`
117
+ );
118
+ return { valid: false, errors };
119
+ }
120
+
121
+ return { valid: true };
122
+ } catch (error: unknown) {
123
+ const errorMessage = error instanceof Error ? error.message : String(error);
124
+ logger.warn(`Failed to validate schema for ${toolKey}:`, errorMessage);
125
+ return { valid: true }; // Skip validation on error
126
+ }
127
+ }
128
+
129
+ async handleFunctionCall(
130
+ duckName: string,
131
+ functionName: string,
132
+ args: Record<string, unknown>
133
+ ): Promise<FunctionCallResult> {
134
+ try {
135
+ logger.info(`FunctionBridge.handleFunctionCall called: ${duckName} -> ${functionName}`);
136
+ logger.debug(`Approval mode: ${this.approvalMode}, Function: ${functionName}`);
137
+
138
+ // Validate that this is an MCP function
139
+ if (!functionName.startsWith('mcp__')) {
140
+ logger.warn(`Invalid function name format: ${functionName}`);
141
+ return {
142
+ success: false,
143
+ error: `Invalid function name: ${functionName}`,
144
+ };
145
+ }
146
+
147
+ // Extract MCP server and tool names from args or function name
148
+ const mcpServer = (args._mcp_server as string) || this.extractServerFromFunctionName(functionName);
149
+ const mcpTool = (args._mcp_tool as string) || this.extractToolFromFunctionName(functionName);
150
+ const approvalId = args._approval_id as string;
151
+
152
+ if (!mcpServer || !mcpTool) {
153
+ return {
154
+ success: false,
155
+ error: `Could not determine MCP server/tool from function: ${functionName}`,
156
+ };
157
+ }
158
+
159
+ // Clean up internal parameters from args
160
+ const cleanArgs = { ...args };
161
+ delete cleanArgs._mcp_server;
162
+ delete cleanArgs._mcp_tool;
163
+ delete cleanArgs._approval_id;
164
+
165
+ // Validate arguments against tool schema
166
+ const toolKey = `${mcpServer}:${mcpTool}`;
167
+ const validation = this.validateToolArguments(toolKey, cleanArgs);
168
+ if (!validation.valid) {
169
+ return {
170
+ success: false,
171
+ error: `Invalid arguments for ${toolKey}: ${validation.errors?.join(', ')}`,
172
+ };
173
+ }
174
+
175
+ // Check if approval is needed based on approval mode
176
+ let isTrusted = false;
177
+
178
+ // First check server-specific trusted tools
179
+ const serverTrustedTools = this.trustedToolsByServer.get(mcpServer);
180
+ if (serverTrustedTools) {
181
+ isTrusted = serverTrustedTools.has('*') || // Wildcard for all tools from server
182
+ serverTrustedTools.has(mcpTool) || // Tool name only
183
+ serverTrustedTools.has(toolKey); // Full server:tool format
184
+ logger.debug(`Server-specific trust check for ${mcpServer}: ${Array.from(serverTrustedTools).join(', ')} - isTrusted: ${isTrusted}`);
185
+ } else {
186
+ // Fall back to global trusted tools
187
+ isTrusted = this.trustedTools.has(toolKey) || this.trustedTools.has(mcpTool);
188
+ logger.debug(`Global trust check - isTrusted: ${isTrusted}`);
189
+ }
190
+
191
+ const isAlreadyApprovedForSession = this.approvalService.isToolApprovedForSession(duckName, mcpServer, mcpTool);
192
+ let needsApproval = false;
193
+
194
+ logger.debug(`Approval check - Mode: ${this.approvalMode}, Trusted: ${isTrusted}, SessionApproved: ${isAlreadyApprovedForSession}, ToolKey: ${toolKey}, Server: ${mcpServer}, ApprovalId: ${approvalId}`);
195
+
196
+ if (this.approvalMode === 'always') {
197
+ // Always require approval unless already approved or approved for session
198
+ needsApproval = !approvalId && !isAlreadyApprovedForSession;
199
+ logger.debug(`Always mode: needsApproval = ${needsApproval}`);
200
+ } else if (this.approvalMode === 'trusted') {
201
+ // Only untrusted tools need approval, unless already approved for session
202
+ needsApproval = !isTrusted && !approvalId && !isAlreadyApprovedForSession;
203
+ logger.debug(`Trusted mode: needsApproval = ${needsApproval}`);
204
+ } else if (this.approvalMode === 'never') {
205
+ // Never require approval
206
+ needsApproval = false;
207
+ logger.debug(`Never mode: needsApproval = ${needsApproval}`);
208
+ }
209
+
210
+ if (needsApproval) {
211
+ // Create approval request
212
+ const request = this.approvalService.createApprovalRequest(
213
+ duckName,
214
+ mcpServer,
215
+ mcpTool,
216
+ cleanArgs
217
+ );
218
+
219
+ return {
220
+ success: false,
221
+ needsApproval: true,
222
+ approvalId: request.id,
223
+ message: `🔒 Approval needed for ${duckName} to call ${mcpServer}:${mcpTool}. Request ID: ${request.id}`,
224
+ };
225
+ }
226
+
227
+ // If approval ID provided, verify it (except in 'never' mode)
228
+ if (approvalId && this.approvalMode !== 'never') {
229
+ const approvalStatus = this.approvalService.getApprovalStatus(approvalId);
230
+
231
+ if (approvalStatus !== 'approved') {
232
+ return {
233
+ success: false,
234
+ error: `Request not approved or expired (status: ${approvalStatus})`,
235
+ };
236
+ }
237
+ }
238
+
239
+ // Execute the MCP tool
240
+ logger.info(`Executing MCP tool ${mcpServer}:${mcpTool} for ${duckName}`);
241
+ const result = await this.mcpManager.callTool(mcpServer, mcpTool, cleanArgs);
242
+
243
+ return {
244
+ success: true,
245
+ data: result,
246
+ };
247
+
248
+ } catch (error: unknown) {
249
+ const errorMessage = error instanceof Error ? error.message : String(error);
250
+ logger.error(`Function call failed for ${functionName}:`, errorMessage);
251
+ return {
252
+ success: false,
253
+ error: `MCP tool execution failed: ${errorMessage}`,
254
+ };
255
+ }
256
+ }
257
+
258
+ private extractServerFromFunctionName(functionName: string): string | null {
259
+ // Function name format: mcp__{server}__{tool}
260
+ const match = functionName.match(/^mcp__([^_]+(?:_[^_]+)*)__/);
261
+ return match ? match[1] : null;
262
+ }
263
+
264
+ private extractToolFromFunctionName(functionName: string): string | null {
265
+ // Function name format: mcp__{server}__{tool}
266
+ const match = functionName.match(/^mcp__[^_]+(?:_[^_]+)*__(.+)$/);
267
+ return match ? match[1] : null;
268
+ }
269
+
270
+ // Check if a specific MCP tool is available
271
+ async isToolAvailable(serverName: string, toolName: string): Promise<boolean> {
272
+ try {
273
+ const tools = await this.mcpManager.listServerTools(serverName);
274
+ return tools.some(tool => tool.name === toolName);
275
+ } catch (error) {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ // Get available MCP tools grouped by server
281
+ async getAvailableToolsByServer(): Promise<Record<string, MCPTool[]>> {
282
+ try {
283
+ const allTools = await this.mcpManager.listAllTools();
284
+ const toolsByServer: Record<string, MCPTool[]> = {};
285
+
286
+ allTools.forEach(tool => {
287
+ if (!toolsByServer[tool.serverName]) {
288
+ toolsByServer[tool.serverName] = [];
289
+ }
290
+ toolsByServer[tool.serverName].push(tool);
291
+ });
292
+
293
+ return toolsByServer;
294
+ } catch (error: unknown) {
295
+ const errorMessage = error instanceof Error ? error.message : String(error);
296
+ logger.error('Failed to get tools by server:', errorMessage);
297
+ return {};
298
+ }
299
+ }
300
+
301
+ // Update trusted tools list
302
+ updateTrustedTools(trustedTools: string[], trustedToolsByServer?: Record<string, string[]>): void {
303
+ this.trustedTools = new Set(trustedTools);
304
+ logger.info(`Updated global trusted tools list: ${Array.from(this.trustedTools).join(', ')}`);
305
+
306
+ if (trustedToolsByServer) {
307
+ this.trustedToolsByServer.clear();
308
+ Object.entries(trustedToolsByServer).forEach(([serverName, tools]) => {
309
+ this.trustedToolsByServer.set(serverName, new Set(tools));
310
+ logger.info(`Updated trusted tools for server ${serverName}: ${tools.join(', ')}`);
311
+ });
312
+ }
313
+ }
314
+
315
+ // Get statistics about function calls
316
+ getStats(): {
317
+ totalFunctions: number;
318
+ serverCount: number;
319
+ trustedToolCount: number;
320
+ connectedServers: string[];
321
+ } {
322
+ return {
323
+ totalFunctions: 0, // Will be populated when tools are loaded
324
+ serverCount: this.mcpManager.getConnectedServers().length,
325
+ trustedToolCount: this.trustedTools.size,
326
+ connectedServers: this.mcpManager.getConnectedServers(),
327
+ };
328
+ }
329
+ }
@@ -0,0 +1,107 @@
1
+ import { ProviderManager } from '../providers/manager.js';
2
+ import { ProviderHealth } from '../config/types.js';
3
+ import { logger } from '../utils/logger.js';
4
+
5
+ export class HealthMonitor {
6
+ private providerManager: ProviderManager;
7
+ private healthCheckInterval: NodeJS.Timeout | null = null;
8
+ private healthCache: Map<string, ProviderHealth> = new Map();
9
+
10
+ constructor(providerManager: ProviderManager) {
11
+ this.providerManager = providerManager;
12
+ }
13
+
14
+ async performHealthChecks(): Promise<Map<string, ProviderHealth>> {
15
+ logger.info('🦆 Performing health checks on all ducks...');
16
+
17
+ const results = await this.providerManager.checkHealth();
18
+
19
+ for (const result of results) {
20
+ this.healthCache.set(result.provider, result);
21
+
22
+ const statusEmoji = result.healthy ? '✅' : '❌';
23
+ const latencyInfo = result.latency ? ` (${result.latency}ms)` : '';
24
+
25
+ logger.info(
26
+ `${statusEmoji} ${result.provider}: ${result.healthy ? 'Healthy' : 'Unhealthy'}${latencyInfo}`
27
+ );
28
+
29
+ if (result.error) {
30
+ logger.warn(` Error: ${result.error}`);
31
+ }
32
+ }
33
+
34
+ return this.healthCache;
35
+ }
36
+
37
+ startMonitoring(intervalMs: number = 60000): void {
38
+ if (this.healthCheckInterval) {
39
+ this.stopMonitoring();
40
+ }
41
+
42
+ // Initial check
43
+ this.performHealthChecks().catch(error => {
44
+ logger.error('Initial health check failed:', error);
45
+ });
46
+
47
+ // Set up periodic checks
48
+ this.healthCheckInterval = setInterval(() => {
49
+ this.performHealthChecks().catch(error => {
50
+ logger.error('Periodic health check failed:', error);
51
+ });
52
+ }, intervalMs);
53
+
54
+ logger.info(`Started health monitoring with ${intervalMs}ms interval`);
55
+ }
56
+
57
+ stopMonitoring(): void {
58
+ if (this.healthCheckInterval) {
59
+ clearInterval(this.healthCheckInterval);
60
+ this.healthCheckInterval = null;
61
+ logger.info('Stopped health monitoring');
62
+ }
63
+ }
64
+
65
+ getHealthStatus(): Map<string, ProviderHealth> {
66
+ return new Map(this.healthCache);
67
+ }
68
+
69
+ getHealthyProviders(): string[] {
70
+ return Array.from(this.healthCache.entries())
71
+ .filter(([_, health]) => health.healthy)
72
+ .map(([provider, _]) => provider);
73
+ }
74
+
75
+ isProviderHealthy(providerName: string): boolean {
76
+ const health = this.healthCache.get(providerName);
77
+ return health?.healthy || false;
78
+ }
79
+
80
+ getProviderLatency(providerName: string): number | undefined {
81
+ const health = this.healthCache.get(providerName);
82
+ return health?.latency;
83
+ }
84
+
85
+ async waitForHealthyProvider(
86
+ maxWaitMs: number = 30000,
87
+ checkIntervalMs: number = 1000
88
+ ): Promise<string | null> {
89
+ const startTime = Date.now();
90
+
91
+ while (Date.now() - startTime < maxWaitMs) {
92
+ const healthyProviders = this.getHealthyProviders();
93
+
94
+ if (healthyProviders.length > 0) {
95
+ return healthyProviders[0];
96
+ }
97
+
98
+ // Perform a fresh health check
99
+ await this.performHealthChecks();
100
+
101
+ // Wait before next check
102
+ await new Promise(resolve => setTimeout(resolve, checkIntervalMs));
103
+ }
104
+
105
+ return null;
106
+ }
107
+ }