mcp-rubber-duck 1.5.1 → 1.6.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 (69) hide show
  1. package/.claude/agents/pricing-updater.md +111 -0
  2. package/.claude/commands/update-pricing.md +22 -0
  3. package/.releaserc.json +4 -0
  4. package/CHANGELOG.md +14 -0
  5. package/dist/config/types.d.ts +72 -0
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/config/types.js +8 -0
  8. package/dist/config/types.js.map +1 -1
  9. package/dist/data/default-pricing.d.ts +18 -0
  10. package/dist/data/default-pricing.d.ts.map +1 -0
  11. package/dist/data/default-pricing.js +307 -0
  12. package/dist/data/default-pricing.js.map +1 -0
  13. package/dist/providers/enhanced-manager.d.ts +2 -1
  14. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  15. package/dist/providers/enhanced-manager.js +20 -2
  16. package/dist/providers/enhanced-manager.js.map +1 -1
  17. package/dist/providers/manager.d.ts +3 -1
  18. package/dist/providers/manager.d.ts.map +1 -1
  19. package/dist/providers/manager.js +12 -1
  20. package/dist/providers/manager.js.map +1 -1
  21. package/dist/server.d.ts +2 -0
  22. package/dist/server.d.ts.map +1 -1
  23. package/dist/server.js +35 -4
  24. package/dist/server.js.map +1 -1
  25. package/dist/services/pricing.d.ts +56 -0
  26. package/dist/services/pricing.d.ts.map +1 -0
  27. package/dist/services/pricing.js +124 -0
  28. package/dist/services/pricing.js.map +1 -0
  29. package/dist/services/usage.d.ts +48 -0
  30. package/dist/services/usage.d.ts.map +1 -0
  31. package/dist/services/usage.js +243 -0
  32. package/dist/services/usage.js.map +1 -0
  33. package/dist/tools/get-usage-stats.d.ts +8 -0
  34. package/dist/tools/get-usage-stats.d.ts.map +1 -0
  35. package/dist/tools/get-usage-stats.js +92 -0
  36. package/dist/tools/get-usage-stats.js.map +1 -0
  37. package/package.json +1 -1
  38. package/src/config/types.ts +51 -0
  39. package/src/data/default-pricing.ts +368 -0
  40. package/src/providers/enhanced-manager.ts +41 -4
  41. package/src/providers/manager.ts +22 -1
  42. package/src/server.ts +42 -4
  43. package/src/services/pricing.ts +155 -0
  44. package/src/services/usage.ts +293 -0
  45. package/src/tools/get-usage-stats.ts +109 -0
  46. package/tests/approval.test.ts +440 -0
  47. package/tests/cache.test.ts +240 -0
  48. package/tests/config.test.ts +468 -0
  49. package/tests/consensus.test.ts +10 -0
  50. package/tests/conversation.test.ts +86 -0
  51. package/tests/duck-debate.test.ts +105 -1
  52. package/tests/duck-iterate.test.ts +30 -0
  53. package/tests/duck-judge.test.ts +93 -0
  54. package/tests/duck-vote.test.ts +46 -0
  55. package/tests/health.test.ts +129 -0
  56. package/tests/pricing.test.ts +335 -0
  57. package/tests/providers.test.ts +591 -0
  58. package/tests/safe-logger.test.ts +314 -0
  59. package/tests/tools/approve-mcp-request.test.ts +239 -0
  60. package/tests/tools/ask-duck.test.ts +159 -0
  61. package/tests/tools/chat-duck.test.ts +191 -0
  62. package/tests/tools/compare-ducks.test.ts +190 -0
  63. package/tests/tools/duck-council.test.ts +219 -0
  64. package/tests/tools/get-pending-approvals.test.ts +195 -0
  65. package/tests/tools/get-usage-stats.test.ts +236 -0
  66. package/tests/tools/list-ducks.test.ts +144 -0
  67. package/tests/tools/list-models.test.ts +163 -0
  68. package/tests/tools/mcp-status.test.ts +330 -0
  69. package/tests/usage.test.ts +661 -0
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { listDucksTool } from '../../src/tools/list-ducks.js';
3
+ import { ProviderManager } from '../../src/providers/manager.js';
4
+ import { HealthMonitor } from '../../src/services/health.js';
5
+
6
+ // Mock dependencies
7
+ jest.mock('../../src/utils/logger');
8
+ jest.mock('../../src/providers/manager.js');
9
+ jest.mock('../../src/services/health.js');
10
+
11
+ describe('listDucksTool', () => {
12
+ let mockProviderManager: jest.Mocked<ProviderManager>;
13
+ let mockHealthMonitor: jest.Mocked<HealthMonitor>;
14
+
15
+ const mockProviders = [
16
+ {
17
+ name: 'openai',
18
+ info: {
19
+ nickname: 'OpenAI Duck',
20
+ model: 'gpt-4',
21
+ baseURL: 'https://api.openai.com/v1',
22
+ hasApiKey: true,
23
+ },
24
+ },
25
+ {
26
+ name: 'groq',
27
+ info: {
28
+ nickname: 'Groq Duck',
29
+ model: 'llama-3.1-70b',
30
+ baseURL: 'https://api.groq.com/openai/v1',
31
+ hasApiKey: true,
32
+ },
33
+ },
34
+ ];
35
+
36
+ beforeEach(() => {
37
+ mockProviderManager = {
38
+ getAllProviders: jest.fn().mockReturnValue(mockProviders),
39
+ } as unknown as jest.Mocked<ProviderManager>;
40
+
41
+ mockHealthMonitor = {
42
+ performHealthChecks: jest.fn(),
43
+ } as unknown as jest.Mocked<HealthMonitor>;
44
+ });
45
+
46
+ it('should list all ducks without health check', async () => {
47
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {});
48
+
49
+ expect(mockProviderManager.getAllProviders).toHaveBeenCalled();
50
+ expect(mockHealthMonitor.performHealthChecks).not.toHaveBeenCalled();
51
+
52
+ expect(result.content).toHaveLength(1);
53
+ expect(result.content[0].type).toBe('text');
54
+ expect(result.content[0].text).toContain('Found 2 duck(s)');
55
+ expect(result.content[0].text).toContain('OpenAI Duck');
56
+ expect(result.content[0].text).toContain('Groq Duck');
57
+ });
58
+
59
+ it('should list ducks with health check when requested', async () => {
60
+ mockHealthMonitor.performHealthChecks.mockResolvedValue([
61
+ {
62
+ provider: 'openai',
63
+ healthy: true,
64
+ latency: 150,
65
+ lastCheck: new Date(),
66
+ },
67
+ {
68
+ provider: 'groq',
69
+ healthy: false,
70
+ lastCheck: new Date(),
71
+ error: 'Connection failed',
72
+ },
73
+ ]);
74
+
75
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {
76
+ check_health: true,
77
+ });
78
+
79
+ expect(mockHealthMonitor.performHealthChecks).toHaveBeenCalled();
80
+ expect(result.content[0].text).toContain('Healthy');
81
+ expect(result.content[0].text).toContain('Unhealthy');
82
+ expect(result.content[0].text).toContain('150ms');
83
+ expect(result.content[0].text).toContain('Connection failed');
84
+ });
85
+
86
+ it('should show unknown status emoji for unchecked providers', async () => {
87
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {
88
+ check_health: false,
89
+ });
90
+
91
+ // Without health check, status should be unknown (❓)
92
+ expect(result.content[0].text).toContain('❓');
93
+ });
94
+
95
+ it('should show healthy count in summary', async () => {
96
+ mockHealthMonitor.performHealthChecks.mockResolvedValue([
97
+ {
98
+ provider: 'openai',
99
+ healthy: true,
100
+ latency: 150,
101
+ lastCheck: new Date(),
102
+ },
103
+ {
104
+ provider: 'groq',
105
+ healthy: true,
106
+ latency: 80,
107
+ lastCheck: new Date(),
108
+ },
109
+ ]);
110
+
111
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {
112
+ check_health: true,
113
+ });
114
+
115
+ expect(result.content[0].text).toContain('2/2 ducks are healthy');
116
+ });
117
+
118
+ it('should handle empty provider list', async () => {
119
+ mockProviderManager.getAllProviders.mockReturnValue([]);
120
+
121
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {});
122
+
123
+ expect(result.content[0].text).toContain('Found 0 duck(s)');
124
+ expect(result.content[0].text).toContain('0/0 ducks are healthy');
125
+ });
126
+
127
+ it('should display provider without API key correctly', async () => {
128
+ mockProviderManager.getAllProviders.mockReturnValue([
129
+ {
130
+ name: 'ollama',
131
+ info: {
132
+ nickname: 'Ollama Duck',
133
+ model: 'llama3',
134
+ baseURL: 'http://localhost:11434/v1',
135
+ hasApiKey: false,
136
+ },
137
+ },
138
+ ]);
139
+
140
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {});
141
+
142
+ expect(result.content[0].text).toContain('Not required');
143
+ });
144
+ });
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { listModelsTool } from '../../src/tools/list-models.js';
3
+ import { ProviderManager } from '../../src/providers/manager.js';
4
+
5
+ // Mock dependencies
6
+ jest.mock('../../src/utils/logger');
7
+ jest.mock('../../src/providers/manager.js');
8
+
9
+ describe('listModelsTool', () => {
10
+ let mockProviderManager: jest.Mocked<ProviderManager>;
11
+
12
+ const mockProviders = [
13
+ {
14
+ name: 'openai',
15
+ info: {
16
+ nickname: 'OpenAI Duck',
17
+ model: 'gpt-4',
18
+ baseURL: 'https://api.openai.com/v1',
19
+ hasApiKey: true,
20
+ },
21
+ },
22
+ {
23
+ name: 'groq',
24
+ info: {
25
+ nickname: 'Groq Duck',
26
+ model: 'llama-3.1-70b-versatile',
27
+ baseURL: 'https://api.groq.com/openai/v1',
28
+ hasApiKey: true,
29
+ },
30
+ },
31
+ ];
32
+
33
+ const mockOpenAIModels = [
34
+ { id: 'gpt-4', description: 'Most capable model', context_window: 8192 },
35
+ { id: 'gpt-3.5-turbo', owned_by: 'openai', context_window: 4096 },
36
+ ];
37
+
38
+ const mockGroqModels = [
39
+ { id: 'llama-3.1-70b-versatile', description: 'Versatile large model' },
40
+ { id: 'llama-3.1-8b-instant', description: 'Fast small model' },
41
+ ];
42
+
43
+ beforeEach(() => {
44
+ mockProviderManager = {
45
+ getAllProviders: jest.fn().mockReturnValue(mockProviders),
46
+ getAvailableModels: jest.fn().mockImplementation((provider) => {
47
+ if (provider === 'openai') return Promise.resolve(mockOpenAIModels);
48
+ if (provider === 'groq') return Promise.resolve(mockGroqModels);
49
+ return Promise.resolve([]);
50
+ }),
51
+ } as unknown as jest.Mocked<ProviderManager>;
52
+ });
53
+
54
+ it('should list models for all providers by default', async () => {
55
+ const result = await listModelsTool(mockProviderManager, {});
56
+
57
+ expect(mockProviderManager.getAllProviders).toHaveBeenCalled();
58
+ expect(mockProviderManager.getAvailableModels).toHaveBeenCalledTimes(2);
59
+ expect(result.content[0].text).toContain('OpenAI Duck');
60
+ expect(result.content[0].text).toContain('Groq Duck');
61
+ });
62
+
63
+ it('should list models for specific provider', async () => {
64
+ const result = await listModelsTool(mockProviderManager, {
65
+ provider: 'openai',
66
+ });
67
+
68
+ expect(mockProviderManager.getAvailableModels).toHaveBeenCalledWith('openai');
69
+ expect(mockProviderManager.getAvailableModels).toHaveBeenCalledTimes(1);
70
+ expect(result.content[0].text).toContain('OpenAI Duck');
71
+ expect(result.content[0].text).not.toContain('Groq Duck');
72
+ });
73
+
74
+ it('should throw error for unknown provider', async () => {
75
+ await expect(
76
+ listModelsTool(mockProviderManager, { provider: 'unknown' })
77
+ ).rejects.toThrow('Provider "unknown" not found');
78
+ });
79
+
80
+ it('should display model details', async () => {
81
+ const result = await listModelsTool(mockProviderManager, {
82
+ provider: 'openai',
83
+ });
84
+
85
+ expect(result.content[0].text).toContain('gpt-4');
86
+ expect(result.content[0].text).toContain('Most capable model');
87
+ expect(result.content[0].text).toContain('8192 tokens');
88
+ });
89
+
90
+ it('should mark default model', async () => {
91
+ const result = await listModelsTool(mockProviderManager, {
92
+ provider: 'openai',
93
+ });
94
+
95
+ expect(result.content[0].text).toContain('gpt-4');
96
+ expect(result.content[0].text).toContain('(default)');
97
+ });
98
+
99
+ it('should display owned_by when no description', async () => {
100
+ const result = await listModelsTool(mockProviderManager, {
101
+ provider: 'openai',
102
+ });
103
+
104
+ expect(result.content[0].text).toContain('by openai');
105
+ });
106
+
107
+ it('should handle empty models list', async () => {
108
+ mockProviderManager.getAvailableModels.mockResolvedValue([]);
109
+
110
+ const result = await listModelsTool(mockProviderManager, {
111
+ provider: 'openai',
112
+ });
113
+
114
+ expect(result.content[0].text).toContain('No models available');
115
+ });
116
+
117
+ it('should handle provider errors gracefully', async () => {
118
+ mockProviderManager.getAvailableModels.mockImplementation((provider) => {
119
+ if (provider === 'openai') return Promise.resolve(mockOpenAIModels);
120
+ return Promise.reject(new Error('Connection failed'));
121
+ });
122
+
123
+ const result = await listModelsTool(mockProviderManager, {});
124
+
125
+ expect(result.content[0].text).toContain('OpenAI Duck');
126
+ expect(result.content[0].text).toContain('Failed to fetch models');
127
+ });
128
+
129
+ it('should show cached indicator when not fetching latest', async () => {
130
+ const result = await listModelsTool(mockProviderManager, {
131
+ fetch_latest: false,
132
+ });
133
+
134
+ expect(result.content[0].text).toContain('Using cached/configured models');
135
+ });
136
+
137
+ it('should show fetch indicator when fetching latest', async () => {
138
+ const result = await listModelsTool(mockProviderManager, {
139
+ fetch_latest: true,
140
+ });
141
+
142
+ expect(result.content[0].text).toContain('Fetched from API');
143
+ });
144
+
145
+ it('should display context window when available', async () => {
146
+ const result = await listModelsTool(mockProviderManager, {
147
+ provider: 'openai',
148
+ });
149
+
150
+ expect(result.content[0].text).toContain('8192 tokens');
151
+ expect(result.content[0].text).toContain('4096 tokens');
152
+ });
153
+
154
+ it('should handle models without context window', async () => {
155
+ const result = await listModelsTool(mockProviderManager, {
156
+ provider: 'groq',
157
+ });
158
+
159
+ // Groq models don't have context_window in mock
160
+ expect(result.content[0].text).toContain('llama-3.1-70b-versatile');
161
+ expect(result.content[0].text).not.toContain('tokens]');
162
+ });
163
+ });
@@ -0,0 +1,330 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { mcpStatusTool } from '../../src/tools/mcp-status.js';
3
+ import { MCPClientManager } from '../../src/services/mcp-client-manager.js';
4
+ import { ApprovalService } from '../../src/services/approval.js';
5
+ import { FunctionBridge } from '../../src/services/function-bridge.js';
6
+
7
+ // Mock dependencies
8
+ jest.mock('../../src/utils/logger');
9
+ jest.mock('../../src/services/mcp-client-manager.js');
10
+ jest.mock('../../src/services/approval.js');
11
+ jest.mock('../../src/services/function-bridge.js');
12
+
13
+ describe('mcpStatusTool', () => {
14
+ let mockMcpManager: jest.Mocked<MCPClientManager>;
15
+ let mockApprovalService: jest.Mocked<ApprovalService>;
16
+ let mockFunctionBridge: jest.Mocked<FunctionBridge>;
17
+
18
+ const mockServerStatus = {
19
+ filesystem: {
20
+ type: 'stdio',
21
+ status: 'connected',
22
+ },
23
+ database: {
24
+ type: 'sse',
25
+ status: 'connecting',
26
+ },
27
+ };
28
+
29
+ const mockTools = [
30
+ { name: 'read_file', serverName: 'filesystem' },
31
+ { name: 'write_file', serverName: 'filesystem' },
32
+ { name: 'query', serverName: 'database' },
33
+ ];
34
+
35
+ const mockApprovalStats = {
36
+ total: 10,
37
+ pending: 2,
38
+ approved: 5,
39
+ denied: 2,
40
+ expired: 1,
41
+ };
42
+
43
+ const mockBridgeStats = {
44
+ trustedToolCount: 5,
45
+ totalCalls: 100,
46
+ successfulCalls: 95,
47
+ };
48
+
49
+ beforeEach(() => {
50
+ mockMcpManager = {
51
+ getStatus: jest.fn().mockReturnValue(mockServerStatus),
52
+ getConnectedServers: jest.fn().mockReturnValue(['filesystem', 'database']),
53
+ listAllTools: jest.fn().mockResolvedValue(mockTools),
54
+ } as unknown as jest.Mocked<MCPClientManager>;
55
+
56
+ mockApprovalService = {
57
+ getStats: jest.fn().mockReturnValue(mockApprovalStats),
58
+ getPendingApprovals: jest.fn().mockReturnValue([]),
59
+ } as unknown as jest.Mocked<ApprovalService>;
60
+
61
+ mockFunctionBridge = {
62
+ getStats: jest.fn().mockReturnValue(mockBridgeStats),
63
+ } as unknown as jest.Mocked<FunctionBridge>;
64
+ });
65
+
66
+ describe('overview section', () => {
67
+ it('should display MCP Bridge status header', async () => {
68
+ const result = await mcpStatusTool(
69
+ mockMcpManager,
70
+ mockApprovalService,
71
+ mockFunctionBridge,
72
+ {}
73
+ );
74
+
75
+ expect(result.content[0].text).toContain('MCP Bridge Status');
76
+ });
77
+
78
+ it('should display connected servers count', async () => {
79
+ const result = await mcpStatusTool(
80
+ mockMcpManager,
81
+ mockApprovalService,
82
+ mockFunctionBridge,
83
+ {}
84
+ );
85
+
86
+ expect(result.content[0].text).toContain('Connected Servers: 2');
87
+ });
88
+
89
+ it('should display available tools count', async () => {
90
+ const result = await mcpStatusTool(
91
+ mockMcpManager,
92
+ mockApprovalService,
93
+ mockFunctionBridge,
94
+ {}
95
+ );
96
+
97
+ expect(result.content[0].text).toContain('Available Tools: 3');
98
+ });
99
+
100
+ it('should display trusted tools count', async () => {
101
+ const result = await mcpStatusTool(
102
+ mockMcpManager,
103
+ mockApprovalService,
104
+ mockFunctionBridge,
105
+ {}
106
+ );
107
+
108
+ expect(result.content[0].text).toContain('Trusted Tools: 5');
109
+ });
110
+ });
111
+
112
+ describe('server details section', () => {
113
+ it('should display server status with icons', async () => {
114
+ const result = await mcpStatusTool(
115
+ mockMcpManager,
116
+ mockApprovalService,
117
+ mockFunctionBridge,
118
+ {}
119
+ );
120
+
121
+ expect(result.content[0].text).toContain('🟢'); // connected
122
+ expect(result.content[0].text).toContain('🟡'); // connecting
123
+ expect(result.content[0].text).toContain('filesystem');
124
+ expect(result.content[0].text).toContain('database');
125
+ });
126
+
127
+ it('should display server type and status', async () => {
128
+ const result = await mcpStatusTool(
129
+ mockMcpManager,
130
+ mockApprovalService,
131
+ mockFunctionBridge,
132
+ {}
133
+ );
134
+
135
+ expect(result.content[0].text).toContain('stdio');
136
+ expect(result.content[0].text).toContain('Status: connected');
137
+ expect(result.content[0].text).toContain('Status: connecting');
138
+ });
139
+
140
+ it('should display tool count per server', async () => {
141
+ const result = await mcpStatusTool(
142
+ mockMcpManager,
143
+ mockApprovalService,
144
+ mockFunctionBridge,
145
+ {}
146
+ );
147
+
148
+ expect(result.content[0].text).toContain('Tools: 2'); // filesystem
149
+ expect(result.content[0].text).toContain('Tools: 1'); // database
150
+ });
151
+
152
+ it('should display tool names for small tool lists', async () => {
153
+ const result = await mcpStatusTool(
154
+ mockMcpManager,
155
+ mockApprovalService,
156
+ mockFunctionBridge,
157
+ {}
158
+ );
159
+
160
+ expect(result.content[0].text).toContain('read_file');
161
+ expect(result.content[0].text).toContain('write_file');
162
+ });
163
+
164
+ it('should truncate long tool lists with count', async () => {
165
+ mockMcpManager.listAllTools.mockResolvedValue([
166
+ { name: 'tool1', serverName: 'server1' },
167
+ { name: 'tool2', serverName: 'server1' },
168
+ { name: 'tool3', serverName: 'server1' },
169
+ { name: 'tool4', serverName: 'server1' },
170
+ { name: 'tool5', serverName: 'server1' },
171
+ ]);
172
+ mockMcpManager.getStatus.mockReturnValue({
173
+ server1: { type: 'stdio', status: 'connected' },
174
+ });
175
+
176
+ const result = await mcpStatusTool(
177
+ mockMcpManager,
178
+ mockApprovalService,
179
+ mockFunctionBridge,
180
+ {}
181
+ );
182
+
183
+ expect(result.content[0].text).toContain('+2 more');
184
+ });
185
+
186
+ it('should handle disconnected server', async () => {
187
+ mockMcpManager.getStatus.mockReturnValue({
188
+ broken: { type: 'stdio', status: 'disconnected' },
189
+ });
190
+
191
+ const result = await mcpStatusTool(
192
+ mockMcpManager,
193
+ mockApprovalService,
194
+ mockFunctionBridge,
195
+ {}
196
+ );
197
+
198
+ expect(result.content[0].text).toContain('🔴');
199
+ });
200
+
201
+ it('should show message when no servers configured', async () => {
202
+ mockMcpManager.getStatus.mockReturnValue({});
203
+ mockMcpManager.listAllTools.mockResolvedValue([]);
204
+ mockMcpManager.getConnectedServers.mockReturnValue([]);
205
+
206
+ const result = await mcpStatusTool(
207
+ mockMcpManager,
208
+ mockApprovalService,
209
+ mockFunctionBridge,
210
+ {}
211
+ );
212
+
213
+ expect(result.content[0].text).toContain('No MCP servers configured');
214
+ });
215
+ });
216
+
217
+ describe('approval statistics section', () => {
218
+ it('should display all approval statistics', async () => {
219
+ const result = await mcpStatusTool(
220
+ mockMcpManager,
221
+ mockApprovalService,
222
+ mockFunctionBridge,
223
+ {}
224
+ );
225
+
226
+ expect(result.content[0].text).toContain('Total Requests: 10');
227
+ expect(result.content[0].text).toContain('Pending: 2');
228
+ expect(result.content[0].text).toContain('Approved: 5');
229
+ expect(result.content[0].text).toContain('Denied: 2');
230
+ expect(result.content[0].text).toContain('Expired: 1');
231
+ });
232
+ });
233
+
234
+ describe('pending approvals section', () => {
235
+ it('should display pending approvals when they exist', async () => {
236
+ const now = Date.now();
237
+ mockApprovalService.getPendingApprovals.mockReturnValue([
238
+ {
239
+ id: 'approval-1',
240
+ duckName: 'TestDuck',
241
+ mcpServer: 'filesystem',
242
+ toolName: 'read_file',
243
+ arguments: {},
244
+ status: 'pending' as const,
245
+ timestamp: now - 30000,
246
+ expiresAt: now + 30000,
247
+ },
248
+ ]);
249
+
250
+ const result = await mcpStatusTool(
251
+ mockMcpManager,
252
+ mockApprovalService,
253
+ mockFunctionBridge,
254
+ {}
255
+ );
256
+
257
+ expect(result.content[0].text).toContain('Pending Approvals');
258
+ expect(result.content[0].text).toContain('TestDuck');
259
+ expect(result.content[0].text).toContain('filesystem:read_file');
260
+ expect(result.content[0].text).toMatch(/\d+s ago/);
261
+ });
262
+
263
+ it('should not display pending section when no pending approvals', async () => {
264
+ mockApprovalService.getStats.mockReturnValue({
265
+ ...mockApprovalStats,
266
+ pending: 0,
267
+ });
268
+
269
+ const result = await mcpStatusTool(
270
+ mockMcpManager,
271
+ mockApprovalService,
272
+ mockFunctionBridge,
273
+ {}
274
+ );
275
+
276
+ expect(result.content[0].text).not.toContain('Pending Approvals:');
277
+ });
278
+ });
279
+
280
+ describe('commands section', () => {
281
+ it('should display available commands', async () => {
282
+ const result = await mcpStatusTool(
283
+ mockMcpManager,
284
+ mockApprovalService,
285
+ mockFunctionBridge,
286
+ {}
287
+ );
288
+
289
+ expect(result.content[0].text).toContain('Commands');
290
+ expect(result.content[0].text).toContain('get_pending_approvals');
291
+ expect(result.content[0].text).toContain('approve_mcp_request');
292
+ expect(result.content[0].text).toContain('ask_duck');
293
+ });
294
+ });
295
+
296
+ describe('error handling', () => {
297
+ it('should handle exceptions gracefully', async () => {
298
+ mockMcpManager.getStatus.mockImplementation(() => {
299
+ throw new Error('Connection failed');
300
+ });
301
+
302
+ const result = await mcpStatusTool(
303
+ mockMcpManager,
304
+ mockApprovalService,
305
+ mockFunctionBridge,
306
+ {}
307
+ );
308
+
309
+ expect(result.isError).toBe(true);
310
+ expect(result.content[0].text).toContain('Failed to get MCP status');
311
+ expect(result.content[0].text).toContain('Connection failed');
312
+ });
313
+
314
+ it('should handle non-Error exceptions', async () => {
315
+ mockMcpManager.getStatus.mockImplementation(() => {
316
+ throw 'Unknown failure';
317
+ });
318
+
319
+ const result = await mcpStatusTool(
320
+ mockMcpManager,
321
+ mockApprovalService,
322
+ mockFunctionBridge,
323
+ {}
324
+ );
325
+
326
+ expect(result.isError).toBe(true);
327
+ expect(result.content[0].text).toContain('Unknown failure');
328
+ });
329
+ });
330
+ });