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,308 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { ConversationManager } from '../src/services/conversation.js';
3
+
4
+ // Mock logger to avoid console noise during tests
5
+ jest.mock('../src/utils/logger');
6
+
7
+ describe('ConversationManager', () => {
8
+ let conversationManager: ConversationManager;
9
+
10
+ beforeEach(() => {
11
+ conversationManager = new ConversationManager();
12
+ });
13
+
14
+ describe('conversation creation and retrieval', () => {
15
+ it('should create a new conversation', () => {
16
+ const conversation = conversationManager.createConversation('test-1', 'openai');
17
+
18
+ expect(conversation.id).toBe('test-1');
19
+ expect(conversation.provider).toBe('openai');
20
+ expect(conversation.messages).toHaveLength(0);
21
+ expect(conversation.createdAt).toBeInstanceOf(Date);
22
+ expect(conversation.updatedAt).toBeInstanceOf(Date);
23
+ });
24
+
25
+ it('should retrieve an existing conversation', () => {
26
+ conversationManager.createConversation('test-1', 'openai');
27
+ const retrieved = conversationManager.getConversation('test-1');
28
+
29
+ expect(retrieved).toBeDefined();
30
+ expect(retrieved!.id).toBe('test-1');
31
+ });
32
+
33
+ it('should return undefined for non-existent conversation', () => {
34
+ const retrieved = conversationManager.getConversation('nonexistent');
35
+ expect(retrieved).toBeUndefined();
36
+ });
37
+ });
38
+
39
+ describe('message handling', () => {
40
+ beforeEach(() => {
41
+ conversationManager.createConversation('test-1', 'openai');
42
+ });
43
+
44
+ it('should add messages to conversation', () => {
45
+ const message = {
46
+ role: 'user' as const,
47
+ content: 'Hello duck',
48
+ timestamp: new Date(),
49
+ };
50
+
51
+ const conversation = conversationManager.addMessage('test-1', message);
52
+
53
+ expect(conversation.messages).toHaveLength(1);
54
+ expect(conversation.messages[0]).toEqual(message);
55
+ expect(conversation.updatedAt).toBeInstanceOf(Date);
56
+ });
57
+
58
+ it('should throw error for non-existent conversation when adding message', () => {
59
+ const message = {
60
+ role: 'user' as const,
61
+ content: 'Hello duck',
62
+ timestamp: new Date(),
63
+ };
64
+
65
+ expect(() => {
66
+ conversationManager.addMessage('nonexistent', message);
67
+ }).toThrow('Conversation nonexistent not found');
68
+ });
69
+
70
+ it('should trim conversations that exceed max size', () => {
71
+ // Add 55 messages (exceeds max of 50)
72
+ for (let i = 0; i < 55; i++) {
73
+ conversationManager.addMessage('test-1', {
74
+ role: 'user' as const,
75
+ content: `Message ${i}`,
76
+ timestamp: new Date(),
77
+ });
78
+ }
79
+
80
+ const conversation = conversationManager.getConversation('test-1');
81
+
82
+ // Should be trimmed to 50 messages
83
+ expect(conversation!.messages).toHaveLength(50);
84
+ // Should keep the latest messages
85
+ expect(conversation!.messages[0].content).toBe('Message 5');
86
+ expect(conversation!.messages[49].content).toBe('Message 54');
87
+ });
88
+ });
89
+
90
+ describe('provider switching', () => {
91
+ beforeEach(() => {
92
+ conversationManager.createConversation('test-1', 'openai');
93
+ });
94
+
95
+ it('should switch provider and add system message', () => {
96
+ const conversation = conversationManager.switchProvider('test-1', 'groq');
97
+
98
+ expect(conversation.provider).toBe('groq');
99
+ expect(conversation.messages).toHaveLength(1);
100
+ expect(conversation.messages[0].role).toBe('system');
101
+ expect(conversation.messages[0].content).toBe('Switched to groq duck');
102
+ expect(conversation.messages[0].provider).toBe('groq');
103
+ });
104
+
105
+ it('should throw error for non-existent conversation when switching provider', () => {
106
+ expect(() => {
107
+ conversationManager.switchProvider('nonexistent', 'groq');
108
+ }).toThrow('Conversation nonexistent not found');
109
+ });
110
+ });
111
+
112
+ describe('conversation context', () => {
113
+ beforeEach(() => {
114
+ conversationManager.createConversation('test-1', 'openai');
115
+
116
+ // Add some messages
117
+ for (let i = 0; i < 5; i++) {
118
+ conversationManager.addMessage('test-1', {
119
+ role: 'user' as const,
120
+ content: `Message ${i}`,
121
+ timestamp: new Date(),
122
+ });
123
+ }
124
+ });
125
+
126
+ it('should return all messages when no limit specified', () => {
127
+ const context = conversationManager.getConversationContext('test-1');
128
+ expect(context).toHaveLength(5);
129
+ });
130
+
131
+ it('should return limited messages when maxMessages specified', () => {
132
+ const context = conversationManager.getConversationContext('test-1', 3);
133
+ expect(context).toHaveLength(3);
134
+ // Should return the last 3 messages
135
+ expect(context[0].content).toBe('Message 2');
136
+ expect(context[2].content).toBe('Message 4');
137
+ });
138
+
139
+ it('should return empty array for non-existent conversation', () => {
140
+ const context = conversationManager.getConversationContext('nonexistent');
141
+ expect(context).toEqual([]);
142
+ });
143
+ });
144
+
145
+ describe('conversation management', () => {
146
+ it('should list all conversations', () => {
147
+ conversationManager.createConversation('test-1', 'openai');
148
+ conversationManager.createConversation('test-2', 'groq');
149
+
150
+ const list = conversationManager.listConversations();
151
+ expect(list).toHaveLength(2);
152
+ expect(list[0].id).toBe('test-1');
153
+ expect(list[1].id).toBe('test-2');
154
+ expect(list[0].messageCount).toBe(0);
155
+ });
156
+
157
+ it('should delete conversation', () => {
158
+ conversationManager.createConversation('test-1', 'openai');
159
+
160
+ const deleted = conversationManager.deleteConversation('test-1');
161
+ expect(deleted).toBe(true);
162
+
163
+ const retrieved = conversationManager.getConversation('test-1');
164
+ expect(retrieved).toBeUndefined();
165
+ });
166
+
167
+ it('should return false when deleting non-existent conversation', () => {
168
+ const deleted = conversationManager.deleteConversation('nonexistent');
169
+ expect(deleted).toBe(false);
170
+ });
171
+ });
172
+
173
+ describe('clearAll functionality', () => {
174
+ it('should clear empty conversation list', () => {
175
+ const result = conversationManager.clearAll();
176
+
177
+ expect(result.conversationsCleared).toBe(0);
178
+ expect(result.messagesCleared).toBe(0);
179
+ });
180
+
181
+ it('should clear single conversation', () => {
182
+ conversationManager.createConversation('test-1', 'openai');
183
+ conversationManager.addMessage('test-1', {
184
+ role: 'user' as const,
185
+ content: 'Hello',
186
+ timestamp: new Date(),
187
+ });
188
+
189
+ const result = conversationManager.clearAll();
190
+
191
+ expect(result.conversationsCleared).toBe(1);
192
+ expect(result.messagesCleared).toBe(1);
193
+
194
+ // Verify conversations are actually cleared
195
+ const retrieved = conversationManager.getConversation('test-1');
196
+ expect(retrieved).toBeUndefined();
197
+ });
198
+
199
+ it('should clear multiple conversations with correct counts', () => {
200
+ // Create first conversation with 2 messages
201
+ conversationManager.createConversation('test-1', 'openai');
202
+ conversationManager.addMessage('test-1', {
203
+ role: 'user' as const,
204
+ content: 'Hello 1',
205
+ timestamp: new Date(),
206
+ });
207
+ conversationManager.addMessage('test-1', {
208
+ role: 'assistant' as const,
209
+ content: 'Hi 1',
210
+ timestamp: new Date(),
211
+ });
212
+
213
+ // Create second conversation with 3 messages
214
+ conversationManager.createConversation('test-2', 'groq');
215
+ conversationManager.addMessage('test-2', {
216
+ role: 'user' as const,
217
+ content: 'Hello 2',
218
+ timestamp: new Date(),
219
+ });
220
+ conversationManager.addMessage('test-2', {
221
+ role: 'assistant' as const,
222
+ content: 'Hi 2',
223
+ timestamp: new Date(),
224
+ });
225
+ conversationManager.addMessage('test-2', {
226
+ role: 'user' as const,
227
+ content: 'Follow up',
228
+ timestamp: new Date(),
229
+ });
230
+
231
+ const result = conversationManager.clearAll();
232
+
233
+ expect(result.conversationsCleared).toBe(2);
234
+ expect(result.messagesCleared).toBe(5);
235
+
236
+ // Verify all conversations are cleared
237
+ expect(conversationManager.getConversation('test-1')).toBeUndefined();
238
+ expect(conversationManager.getConversation('test-2')).toBeUndefined();
239
+ expect(conversationManager.listConversations()).toHaveLength(0);
240
+ });
241
+
242
+ it('should allow new conversations after clear', () => {
243
+ // Create and clear
244
+ conversationManager.createConversation('test-1', 'openai');
245
+ conversationManager.clearAll();
246
+
247
+ // Create new conversation after clear
248
+ const newConversation = conversationManager.createConversation('test-2', 'groq');
249
+
250
+ expect(newConversation.id).toBe('test-2');
251
+ expect(newConversation.provider).toBe('groq');
252
+ expect(conversationManager.listConversations()).toHaveLength(1);
253
+ });
254
+ });
255
+
256
+ describe('conversation persistence', () => {
257
+ it('should maintain conversation between message additions', () => {
258
+ conversationManager.createConversation('test-1', 'openai');
259
+
260
+ // Add first message
261
+ conversationManager.addMessage('test-1', {
262
+ role: 'user' as const,
263
+ content: 'First message',
264
+ timestamp: new Date(),
265
+ });
266
+
267
+ // Add second message
268
+ conversationManager.addMessage('test-1', {
269
+ role: 'assistant' as const,
270
+ content: 'First response',
271
+ timestamp: new Date(),
272
+ });
273
+
274
+ const conversation = conversationManager.getConversation('test-1');
275
+ expect(conversation!.messages).toHaveLength(2);
276
+ expect(conversation!.messages[0].content).toBe('First message');
277
+ expect(conversation!.messages[1].content).toBe('First response');
278
+ });
279
+
280
+ it('should handle provider switching without losing messages', () => {
281
+ conversationManager.createConversation('test-1', 'openai');
282
+
283
+ // Add initial message
284
+ conversationManager.addMessage('test-1', {
285
+ role: 'user' as const,
286
+ content: 'Before switch',
287
+ timestamp: new Date(),
288
+ });
289
+
290
+ // Switch provider
291
+ conversationManager.switchProvider('test-1', 'groq');
292
+
293
+ // Add message after switch
294
+ conversationManager.addMessage('test-1', {
295
+ role: 'user' as const,
296
+ content: 'After switch',
297
+ timestamp: new Date(),
298
+ });
299
+
300
+ const conversation = conversationManager.getConversation('test-1');
301
+ expect(conversation!.messages).toHaveLength(3);
302
+ expect(conversation!.messages[0].content).toBe('Before switch');
303
+ expect(conversation!.messages[1].content).toBe('Switched to groq duck');
304
+ expect(conversation!.messages[2].content).toBe('After switch');
305
+ expect(conversation!.provider).toBe('groq');
306
+ });
307
+ });
308
+ });
@@ -0,0 +1,291 @@
1
+ import { jest } from '@jest/globals';
2
+ import { ApprovalService } from '../src/services/approval';
3
+ import { FunctionBridge } from '../src/services/function-bridge';
4
+ import { MCPClientManager } from '../src/services/mcp-client-manager';
5
+
6
+ describe('MCP Bridge', () => {
7
+ let approvalService: ApprovalService;
8
+ let mcpManager: MCPClientManager;
9
+ let functionBridge: FunctionBridge;
10
+
11
+ beforeEach(() => {
12
+ approvalService = new ApprovalService(300); // 5 minutes
13
+ mcpManager = new MCPClientManager([]); // Empty config for testing
14
+ functionBridge = new FunctionBridge(mcpManager, approvalService, []);
15
+ });
16
+
17
+ afterEach(() => {
18
+ approvalService.shutdown();
19
+ });
20
+
21
+ describe('ApprovalService', () => {
22
+ it('should create approval requests', () => {
23
+ const request = approvalService.createApprovalRequest(
24
+ 'TestDuck',
25
+ 'filesystem',
26
+ 'read_file',
27
+ { path: '/test.txt' }
28
+ );
29
+
30
+ expect(request).toBeDefined();
31
+ expect(request.duckName).toBe('TestDuck');
32
+ expect(request.mcpServer).toBe('filesystem');
33
+ expect(request.toolName).toBe('read_file');
34
+ expect(request.status).toBe('pending');
35
+ });
36
+
37
+ it('should approve pending requests', () => {
38
+ const request = approvalService.createApprovalRequest(
39
+ 'TestDuck',
40
+ 'filesystem',
41
+ 'read_file',
42
+ { path: '/test.txt' }
43
+ );
44
+
45
+ const approved = approvalService.approveRequest(request.id);
46
+ expect(approved).toBe(true);
47
+
48
+ const status = approvalService.getApprovalStatus(request.id);
49
+ expect(status).toBe('approved');
50
+ });
51
+
52
+ it('should deny requests', () => {
53
+ const request = approvalService.createApprovalRequest(
54
+ 'TestDuck',
55
+ 'filesystem',
56
+ 'read_file',
57
+ { path: '/test.txt' }
58
+ );
59
+
60
+ const denied = approvalService.denyRequest(request.id, 'Security concern');
61
+ expect(denied).toBe(true);
62
+
63
+ const status = approvalService.getApprovalStatus(request.id);
64
+ expect(status).toBe('denied');
65
+ });
66
+
67
+ it('should handle non-existent requests', () => {
68
+ const status = approvalService.getApprovalStatus('non-existent');
69
+ expect(status).toBeUndefined();
70
+
71
+ const approved = approvalService.approveRequest('non-existent');
72
+ expect(approved).toBe(false);
73
+ });
74
+
75
+ it('should get pending approvals', () => {
76
+ approvalService.createApprovalRequest('Duck1', 'server1', 'tool1', {});
77
+ approvalService.createApprovalRequest('Duck2', 'server2', 'tool2', {});
78
+
79
+ const pending = approvalService.getPendingApprovals();
80
+ expect(pending).toHaveLength(2);
81
+ expect(pending.every(req => req.status === 'pending')).toBe(true);
82
+ });
83
+ });
84
+
85
+ describe('FunctionBridge', () => {
86
+ it('should generate function definitions for empty MCP tools', async () => {
87
+ const functions = await functionBridge.getFunctionDefinitions();
88
+ expect(Array.isArray(functions)).toBe(true);
89
+ // Should be empty since we have no MCP servers configured
90
+ expect(functions).toHaveLength(0);
91
+ });
92
+
93
+ it('should handle function calls requiring approval', async () => {
94
+ const result = await functionBridge.handleFunctionCall(
95
+ 'TestDuck',
96
+ 'mcp__filesystem__read_file',
97
+ { path: '/test.txt', _mcp_server: 'filesystem', _mcp_tool: 'read_file' }
98
+ );
99
+
100
+ expect(result.success).toBe(false);
101
+ expect(result.needsApproval).toBe(true);
102
+ expect(result.approvalId).toBeDefined();
103
+ });
104
+
105
+ it('should handle invalid function names', async () => {
106
+ const result = await functionBridge.handleFunctionCall(
107
+ 'TestDuck',
108
+ 'invalid_function',
109
+ {}
110
+ );
111
+
112
+ expect(result.success).toBe(false);
113
+ expect(result.error).toContain('Invalid function name');
114
+ });
115
+
116
+ it('should validate tool arguments', async () => {
117
+ // Mock a tool with schema in the functionBridge
118
+ const mockTool = {
119
+ serverName: 'test_server',
120
+ name: 'test_tool',
121
+ description: 'Test tool',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ required_param: { type: 'string' }
126
+ },
127
+ required: ['required_param']
128
+ }
129
+ };
130
+
131
+ // Add the tool schema manually for testing
132
+ (functionBridge as any).toolSchemas.set('test_server:test_tool', mockTool.inputSchema);
133
+
134
+ const result = await functionBridge.handleFunctionCall(
135
+ 'TestDuck',
136
+ 'mcp__test_server__test_tool',
137
+ {
138
+ _mcp_server: 'test_server',
139
+ _mcp_tool: 'test_tool',
140
+ // Missing required_param
141
+ }
142
+ );
143
+
144
+ expect(result.success).toBe(false);
145
+ expect(result.error).toContain('Invalid arguments');
146
+ });
147
+
148
+ it('should handle underscored tool names correctly', async () => {
149
+ // Test that tool names with underscores are extracted correctly
150
+ const serverName = (functionBridge as any).extractServerFromFunctionName('mcp__file_system__read_file');
151
+ const toolName = (functionBridge as any).extractToolFromFunctionName('mcp__file_system__read_file');
152
+
153
+ expect(serverName).toBe('file_system');
154
+ expect(toolName).toBe('read_file');
155
+ });
156
+
157
+ it('should handle complex server and tool names', async () => {
158
+ // Test more complex names
159
+ const serverName = (functionBridge as any).extractServerFromFunctionName('mcp__complex_server_name__complex_tool_name');
160
+ const toolName = (functionBridge as any).extractToolFromFunctionName('mcp__complex_server_name__complex_tool_name');
161
+
162
+ expect(serverName).toBe('complex_server_name');
163
+ expect(toolName).toBe('complex_tool_name');
164
+ });
165
+ });
166
+
167
+ describe('MCPClientManager', () => {
168
+ it('should initialize with empty config', async () => {
169
+ expect(mcpManager.getConnectedServers()).toEqual([]);
170
+ expect(mcpManager.getConnectionStatus('nonexistent')).toBe('unknown');
171
+ });
172
+
173
+ it('should handle health check with no servers', async () => {
174
+ const health = await mcpManager.healthCheck();
175
+ expect(health).toEqual({});
176
+ });
177
+
178
+ it('should get status of all servers', () => {
179
+ const status = mcpManager.getStatus();
180
+ expect(typeof status).toBe('object');
181
+ });
182
+ });
183
+
184
+ describe('Integration Tests', () => {
185
+ it('should handle complete approval workflow', async () => {
186
+ // Create approval request
187
+ const request = approvalService.createApprovalRequest(
188
+ 'TestDuck',
189
+ 'filesystem',
190
+ 'read_file',
191
+ { path: '/test.txt' }
192
+ );
193
+
194
+ expect(request.status).toBe('pending');
195
+
196
+ // Try function call without approval
197
+ const result1 = await functionBridge.handleFunctionCall(
198
+ 'TestDuck',
199
+ 'mcp__filesystem__read_file',
200
+ {
201
+ path: '/test.txt',
202
+ _mcp_server: 'filesystem',
203
+ _mcp_tool: 'read_file'
204
+ }
205
+ );
206
+
207
+ expect(result1.success).toBe(false);
208
+ expect(result1.needsApproval).toBe(true);
209
+
210
+ // Approve the request
211
+ const approved = approvalService.approveRequest(request.id);
212
+ expect(approved).toBe(true);
213
+
214
+ // Try function call with approval (would still fail due to no MCP server, but approval logic works)
215
+ const result2 = await functionBridge.handleFunctionCall(
216
+ 'TestDuck',
217
+ 'mcp__filesystem__read_file',
218
+ {
219
+ path: '/test.txt',
220
+ _mcp_server: 'filesystem',
221
+ _mcp_tool: 'read_file',
222
+ _approval_id: request.id
223
+ }
224
+ );
225
+
226
+ // Should pass approval but fail on MCP execution
227
+ expect(result2.error).toContain('MCP server filesystem not connected');
228
+ });
229
+
230
+ it('should handle expired approvals', (done) => {
231
+ // Create approval service with very short timeout
232
+ const shortApprovalService = new ApprovalService(1); // 1 second
233
+
234
+ const request = shortApprovalService.createApprovalRequest(
235
+ 'TestDuck',
236
+ 'filesystem',
237
+ 'read_file',
238
+ { path: '/test.txt' }
239
+ );
240
+
241
+ // Wait for expiration
242
+ setTimeout(() => {
243
+ const status = shortApprovalService.getApprovalStatus(request.id);
244
+ expect(status).toBe('expired');
245
+
246
+ // Try to approve expired request
247
+ const approved = shortApprovalService.approveRequest(request.id);
248
+ expect(approved).toBe(false);
249
+
250
+ shortApprovalService.shutdown();
251
+ done();
252
+ }, 1100);
253
+ });
254
+
255
+ it('should handle function definition generation', async () => {
256
+ // Mock MCP tools
257
+ const mockTools = [
258
+ {
259
+ serverName: 'filesystem',
260
+ name: 'read_file',
261
+ description: 'Read a file',
262
+ inputSchema: {
263
+ type: 'object',
264
+ properties: { path: { type: 'string' } },
265
+ required: ['path']
266
+ }
267
+ },
268
+ {
269
+ serverName: 'web',
270
+ name: 'fetch_url',
271
+ description: 'Fetch URL content',
272
+ inputSchema: {
273
+ type: 'object',
274
+ properties: { url: { type: 'string' } },
275
+ required: ['url']
276
+ }
277
+ }
278
+ ];
279
+
280
+ // Mock the listAllTools method
281
+ jest.spyOn(mcpManager, 'listAllTools').mockResolvedValue(mockTools);
282
+
283
+ const functions = await functionBridge.getFunctionDefinitions();
284
+
285
+ expect(functions).toHaveLength(2);
286
+ expect(functions[0].name).toBe('mcp__filesystem__read_file');
287
+ expect(functions[1].name).toBe('mcp__web__fetch_url');
288
+ expect(functions[0].description).toBe('[filesystem] Read a file');
289
+ });
290
+ });
291
+ });