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,357 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import * as dotenv from 'dotenv';
4
+ import { Config, ConfigSchema, ProviderConfig, MCPBridgeConfig, MCPServerConfig } from './types.js';
5
+ import { logger } from '../utils/logger.js';
6
+
7
+ dotenv.config();
8
+
9
+ export class ConfigManager {
10
+ private config: Config;
11
+ private configPath: string;
12
+
13
+ constructor(configPath?: string) {
14
+ this.configPath = configPath || this.findConfigFile();
15
+ this.config = this.loadConfig();
16
+ }
17
+
18
+ private findConfigFile(): string {
19
+ const possiblePaths = [
20
+ join(process.cwd(), 'config', 'config.json'),
21
+ join(process.cwd(), 'config.json'),
22
+ join(process.env.HOME || '', '.mcp-rubber-duck', 'config.json'),
23
+ ];
24
+
25
+ for (const path of possiblePaths) {
26
+ if (existsSync(path)) {
27
+ logger.info(`Found config file at: ${path}`);
28
+ return path;
29
+ }
30
+ }
31
+
32
+ // If no config file found, use default config
33
+ logger.warn('No config file found, using environment variables and defaults');
34
+ return '';
35
+ }
36
+
37
+ private loadConfig(): Config {
38
+ let rawConfig: Record<string, unknown> = {};
39
+
40
+ // Load from file if exists
41
+ if (this.configPath && existsSync(this.configPath)) {
42
+ try {
43
+ const fileContent = readFileSync(this.configPath, 'utf-8');
44
+ rawConfig = JSON.parse(fileContent) as Record<string, unknown>;
45
+ } catch (error) {
46
+ logger.error(`Failed to load config file: ${String(error)}`);
47
+ }
48
+ }
49
+
50
+ // Merge with environment variables
51
+ rawConfig = this.mergeWithEnv(rawConfig);
52
+
53
+ // Add default providers if none configured
54
+ if (!rawConfig.providers || Object.keys(rawConfig.providers).length === 0) {
55
+ rawConfig.providers = this.getDefaultProviders();
56
+ }
57
+
58
+ // Validate and parse config
59
+ try {
60
+ const config = ConfigSchema.parse(rawConfig);
61
+
62
+ // Set default provider if not specified
63
+ if (!config.default_provider && Object.keys(config.providers).length > 0) {
64
+ config.default_provider = Object.keys(config.providers)[0];
65
+ }
66
+
67
+ return config;
68
+ } catch (error) {
69
+ logger.error(`Invalid configuration: ${String(error)}`);
70
+ throw new Error(`Configuration validation failed: ${String(error)}`);
71
+ }
72
+ }
73
+
74
+ private mergeWithEnv(config: Record<string, unknown>): Record<string, unknown> {
75
+ // Replace ${ENV_VAR} patterns with actual environment values
76
+ const configStr = JSON.stringify(config);
77
+ const replaced = configStr.replace(/\$\{([^}]+)\}/g, (match, envVar: string) => {
78
+ const value = process.env[envVar];
79
+ if (!value && envVar.includes('API_KEY')) {
80
+ logger.warn(`Environment variable ${envVar} not found`);
81
+ }
82
+ return value || match;
83
+ });
84
+
85
+ const merged = JSON.parse(replaced) as Record<string, unknown>;
86
+
87
+ // Apply environment overrides
88
+ if (process.env.DEFAULT_PROVIDER) {
89
+ merged.default_provider = process.env.DEFAULT_PROVIDER;
90
+ }
91
+ if (process.env.DEFAULT_TEMPERATURE) {
92
+ merged.default_temperature = parseFloat(process.env.DEFAULT_TEMPERATURE);
93
+ }
94
+ if (process.env.LOG_LEVEL) {
95
+ merged.log_level = process.env.LOG_LEVEL;
96
+ }
97
+
98
+ // Apply MCP bridge configuration from environment
99
+ merged.mcp_bridge = this.getMCPBridgeConfig(merged.mcp_bridge as Partial<MCPBridgeConfig>);
100
+
101
+ return merged;
102
+ }
103
+
104
+ private getDefaultProviders(): Record<string, ProviderConfig> {
105
+ const providers: Record<string, ProviderConfig> = {};
106
+
107
+ // OpenAI
108
+ if (process.env.OPENAI_API_KEY) {
109
+ providers.openai = {
110
+ api_key: process.env.OPENAI_API_KEY,
111
+ base_url: 'https://api.openai.com/v1',
112
+ models: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'],
113
+ default_model: process.env.OPENAI_DEFAULT_MODEL || 'gpt-4o-mini',
114
+ nickname: process.env.OPENAI_NICKNAME || 'GPT Duck',
115
+ };
116
+ }
117
+
118
+ // Google Gemini
119
+ if (process.env.GEMINI_API_KEY) {
120
+ providers.gemini = {
121
+ api_key: process.env.GEMINI_API_KEY,
122
+ base_url: 'https://generativelanguage.googleapis.com/v1beta/openai/',
123
+ models: ['gemini-2.5-flash', 'gemini-2.0-flash'],
124
+ default_model: process.env.GEMINI_DEFAULT_MODEL || 'gemini-2.5-flash',
125
+ nickname: process.env.GEMINI_NICKNAME || 'Gemini Duck',
126
+ };
127
+ }
128
+
129
+ // Groq
130
+ if (process.env.GROQ_API_KEY) {
131
+ providers.groq = {
132
+ api_key: process.env.GROQ_API_KEY,
133
+ base_url: 'https://api.groq.com/openai/v1',
134
+ models: ['llama-3.3-70b-versatile', 'mixtral-8x7b-32768'],
135
+ default_model: process.env.GROQ_DEFAULT_MODEL || 'llama-3.3-70b-versatile',
136
+ nickname: process.env.GROQ_NICKNAME || 'Groq Duck',
137
+ };
138
+ }
139
+
140
+ // Local Ollama (only if explicitly configured)
141
+ if (process.env.OLLAMA_BASE_URL || process.env.ENABLE_OLLAMA === 'true') {
142
+ providers.ollama = {
143
+ api_key: 'not-needed',
144
+ base_url: process.env.OLLAMA_BASE_URL || 'http://localhost:11434/v1',
145
+ models: ['llama3.2', 'mistral', 'codellama'],
146
+ default_model: process.env.OLLAMA_DEFAULT_MODEL || 'llama3.2',
147
+ nickname: process.env.OLLAMA_NICKNAME || 'Local Duck',
148
+ };
149
+ }
150
+
151
+ // Add all custom providers from environment
152
+ const customProviders = this.getCustomProvidersFromEnv();
153
+ Object.assign(providers, customProviders);
154
+
155
+ return providers;
156
+ }
157
+
158
+ private getMCPBridgeConfig(existingConfig: Partial<MCPBridgeConfig> = {}): Partial<MCPBridgeConfig> {
159
+ // Don't override if MCP is explicitly disabled
160
+ if (existingConfig?.enabled === false) {
161
+ return existingConfig;
162
+ }
163
+
164
+ const mcpConfig: Partial<MCPBridgeConfig> = { ...existingConfig };
165
+
166
+ // Enable MCP bridge if environment variables are present
167
+ if (process.env.MCP_BRIDGE_ENABLED !== undefined) {
168
+ mcpConfig.enabled = process.env.MCP_BRIDGE_ENABLED === 'true';
169
+ } else if (this.hasMCPServerConfig()) {
170
+ mcpConfig.enabled = true;
171
+ }
172
+
173
+ // Apply MCP bridge settings
174
+ if (process.env.MCP_APPROVAL_MODE) {
175
+ const approvalMode = process.env.MCP_APPROVAL_MODE;
176
+ if (approvalMode === 'always' || approvalMode === 'trusted' || approvalMode === 'never') {
177
+ mcpConfig.approval_mode = approvalMode;
178
+ }
179
+ }
180
+ if (process.env.MCP_APPROVAL_TIMEOUT) {
181
+ mcpConfig.approval_timeout = parseInt(process.env.MCP_APPROVAL_TIMEOUT);
182
+ }
183
+ if (process.env.MCP_TRUSTED_TOOLS) {
184
+ mcpConfig.trusted_tools = process.env.MCP_TRUSTED_TOOLS.split(',').map(t => t.trim());
185
+ }
186
+
187
+ // Parse per-server trusted tools from environment
188
+ mcpConfig.trusted_tools_by_server = this.getTrustedToolsByServerFromEnv();
189
+
190
+ // Configure MCP servers from environment
191
+ mcpConfig.mcp_servers = this.getMCPServersFromEnv();
192
+
193
+ return mcpConfig.enabled || mcpConfig.mcp_servers?.length > 0 ? mcpConfig : existingConfig;
194
+ }
195
+
196
+ private hasMCPServerConfig(): boolean {
197
+ // Check if any MCP server environment variables are present
198
+ return Object.keys(process.env).some(key => key.startsWith('MCP_SERVER_'));
199
+ }
200
+
201
+ private getTrustedToolsByServerFromEnv(): Record<string, string[]> {
202
+ const trustedToolsByServer: Record<string, string[]> = {};
203
+
204
+ // Look for environment variables matching MCP_TRUSTED_TOOLS_{SERVER_NAME}
205
+ Object.keys(process.env).forEach(key => {
206
+ const match = key.match(/^MCP_TRUSTED_TOOLS_(.+)$/);
207
+ if (match) {
208
+ const serverName = match[1].toLowerCase().replace(/_/g, '-');
209
+ const toolsStr = process.env[key];
210
+
211
+ if (toolsStr) {
212
+ if (toolsStr.trim() === '*') {
213
+ // Wildcard: trust all tools from this server
214
+ trustedToolsByServer[serverName] = ['*'];
215
+ } else {
216
+ // Parse comma-separated list of tools
217
+ trustedToolsByServer[serverName] = toolsStr.split(',').map(tool => tool.trim());
218
+ }
219
+
220
+ logger.info(`Found trusted tools for server ${serverName}: ${JSON.stringify(trustedToolsByServer[serverName])}`);
221
+ }
222
+ }
223
+ });
224
+
225
+ return trustedToolsByServer;
226
+ }
227
+
228
+ private getCustomProvidersFromEnv(): Record<string, ProviderConfig> {
229
+ const customProviders: Record<string, ProviderConfig> = {};
230
+ const providerNames = new Set<string>();
231
+
232
+ // Find all custom provider configurations
233
+ Object.keys(process.env).forEach(key => {
234
+ const match = key.match(/^CUSTOM_(.+)_(API_KEY|BASE_URL|MODELS|DEFAULT_MODEL|NICKNAME)$/);
235
+ if (match) {
236
+ const providerName = match[1];
237
+ providerNames.add(providerName);
238
+ }
239
+ });
240
+
241
+ // Build provider configurations
242
+ providerNames.forEach(providerName => {
243
+ const prefix = `CUSTOM_${providerName}_`;
244
+ const apiKey = process.env[`${prefix}API_KEY`];
245
+ const baseUrl = process.env[`${prefix}BASE_URL`];
246
+
247
+ // Both API_KEY and BASE_URL are required
248
+ if (apiKey && baseUrl) {
249
+ const providerKey = providerName.toLowerCase();
250
+
251
+ const modelsStr = process.env[`${prefix}MODELS`];
252
+ const models = modelsStr && modelsStr.trim() ?
253
+ modelsStr.split(',').map(m => m.trim()).filter(m => m.length > 0) :
254
+ ['custom-model'];
255
+
256
+ customProviders[providerKey] = {
257
+ api_key: apiKey,
258
+ base_url: baseUrl,
259
+ models: models.length > 0 ? models : ['custom-model'],
260
+ default_model: process.env[`${prefix}DEFAULT_MODEL`] || 'custom-model',
261
+ nickname: process.env[`${prefix}NICKNAME`] || `${providerName} Duck`,
262
+ };
263
+ }
264
+ });
265
+
266
+ return customProviders;
267
+ }
268
+
269
+ private getMCPServersFromEnv(): MCPServerConfig[] {
270
+ const servers: MCPServerConfig[] = [];
271
+ const serverNames = new Set<string>();
272
+
273
+ // Find all MCP server configurations
274
+ Object.keys(process.env).forEach(key => {
275
+ const match = key.match(/^MCP_SERVER_(.+)_(.+)$/);
276
+ if (match) {
277
+ serverNames.add(match[1]);
278
+ }
279
+ });
280
+
281
+ // Build server configurations
282
+ serverNames.forEach(serverName => {
283
+ const prefix = `MCP_SERVER_${serverName}_`;
284
+ const type = process.env[`${prefix}TYPE`];
285
+ const command = process.env[`${prefix}COMMAND`];
286
+ const url = process.env[`${prefix}URL`];
287
+
288
+ // For stdio servers, we need type and command
289
+ // For http servers, we need type and url
290
+ if (type && ((type === 'stdio' && command) || (type === 'http' && url))) {
291
+ const server: Partial<MCPServerConfig> = {
292
+ name: serverName.toLowerCase().replace(/_/g, '-'),
293
+ type: type as 'stdio' | 'http',
294
+ enabled: process.env[`${prefix}ENABLED`] !== 'false',
295
+ };
296
+
297
+ // Add command for stdio servers
298
+ if (type === 'stdio' && command) {
299
+ server.command = command;
300
+ }
301
+
302
+ // Optional arguments
303
+ const argsEnv = process.env[`${prefix}ARGS`];
304
+ if (argsEnv) {
305
+ server.args = argsEnv.split(',').map(arg => arg.trim());
306
+ }
307
+
308
+ // Add URL for http servers (required) and stdio servers (optional)
309
+ if (process.env[`${prefix}URL`]) {
310
+ server.url = process.env[`${prefix}URL`];
311
+ }
312
+
313
+ // Optional API key
314
+ if (process.env[`${prefix}API_KEY`]) {
315
+ server.apiKey = process.env[`${prefix}API_KEY`];
316
+ }
317
+
318
+ // Retry configuration
319
+ const retryAttemptsEnv = process.env[`${prefix}RETRY_ATTEMPTS`];
320
+ if (retryAttemptsEnv) {
321
+ server.retryAttempts = parseInt(retryAttemptsEnv);
322
+ }
323
+ const retryDelayEnv = process.env[`${prefix}RETRY_DELAY`];
324
+ if (retryDelayEnv) {
325
+ server.retryDelay = parseInt(retryDelayEnv);
326
+ }
327
+
328
+ servers.push(server as MCPServerConfig);
329
+ }
330
+ });
331
+
332
+ return servers;
333
+ }
334
+
335
+ getConfig(): Config {
336
+ return this.config;
337
+ }
338
+
339
+ getProvider(name: string) {
340
+ return this.config.providers[name];
341
+ }
342
+
343
+ getDefaultProvider() {
344
+ if (!this.config.default_provider) {
345
+ throw new Error('No default provider configured');
346
+ }
347
+ return this.config.providers[this.config.default_provider];
348
+ }
349
+
350
+ getAllProviders() {
351
+ return this.config.providers;
352
+ }
353
+
354
+ updateConfig(updates: Partial<Config>) {
355
+ this.config = { ...this.config, ...updates };
356
+ }
357
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ProviderConfigSchema = z.object({
4
+ api_key: z.string().optional(),
5
+ base_url: z.string().url(),
6
+ models: z.array(z.string()),
7
+ default_model: z.string(),
8
+ nickname: z.string(),
9
+ temperature: z.number().min(0).max(2).optional(),
10
+ system_prompt: z.string().optional(),
11
+ timeout: z.number().positive().optional(),
12
+ max_retries: z.number().min(0).max(5).optional(),
13
+ });
14
+
15
+ export const MCPServerConfigSchema = z.object({
16
+ name: z.string(),
17
+ type: z.enum(['stdio', 'http']),
18
+ command: z.string().optional(),
19
+ args: z.array(z.string()).optional(),
20
+ url: z.string().url().optional(),
21
+ apiKey: z.string().optional(),
22
+ enabled: z.boolean().default(true),
23
+ retryAttempts: z.number().min(0).max(10).default(3),
24
+ retryDelay: z.number().min(100).max(30000).default(1000),
25
+ });
26
+
27
+ export const MCPBridgeConfigSchema = z.object({
28
+ enabled: z.boolean().default(false),
29
+ approval_mode: z.enum(['always', 'trusted', 'never']).default('always'),
30
+ approval_timeout: z.number().min(30).max(3600).default(300), // 5 minutes
31
+ trusted_tools: z.array(z.string()).default([]), // Global fallback trusted tools
32
+ trusted_tools_by_server: z.record(z.string(), z.array(z.string())).optional(), // Per-server trusted tools
33
+ mcp_servers: z.array(MCPServerConfigSchema).default([]),
34
+ });
35
+
36
+ export const ConfigSchema = z.object({
37
+ providers: z.record(z.string(), ProviderConfigSchema),
38
+ default_provider: z.string().optional(),
39
+ default_temperature: z.number().min(0).max(2).default(0.7),
40
+ cache_ttl: z.number().min(0).default(300), // 5 minutes
41
+ enable_failover: z.boolean().default(true),
42
+ log_level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
43
+ mcp_bridge: MCPBridgeConfigSchema.optional(),
44
+ });
45
+
46
+ export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
47
+ export type MCPServerConfig = z.infer<typeof MCPServerConfigSchema>;
48
+ export type MCPBridgeConfig = z.infer<typeof MCPBridgeConfigSchema>;
49
+ export type Config = z.infer<typeof ConfigSchema>;
50
+
51
+ export interface ConversationMessage {
52
+ role: 'system' | 'user' | 'assistant';
53
+ content: string;
54
+ timestamp: Date;
55
+ provider?: string;
56
+ }
57
+
58
+ export interface Conversation {
59
+ id: string;
60
+ messages: ConversationMessage[];
61
+ provider: string;
62
+ createdAt: Date;
63
+ updatedAt: Date;
64
+ }
65
+
66
+ export interface ProviderHealth {
67
+ provider: string;
68
+ healthy: boolean;
69
+ latency?: number;
70
+ lastCheck: Date;
71
+ error?: string;
72
+ }
73
+
74
+ export interface DuckResponse {
75
+ provider: string;
76
+ nickname: string;
77
+ model: string;
78
+ content: string;
79
+ usage?: {
80
+ prompt_tokens: number;
81
+ completion_tokens: number;
82
+ total_tokens: number;
83
+ promptTokens?: number;
84
+ completionTokens?: number;
85
+ totalTokens?: number;
86
+ };
87
+ latency: number;
88
+ cached: boolean;
89
+ }
package/src/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { RubberDuckServer } from './server.js';
4
+ import { logger } from './utils/logger.js';
5
+
6
+ // Global error handlers for crash diagnosis
7
+ process.on('uncaughtException', (error) => {
8
+ logger.error('FATAL: Uncaught Exception', {
9
+ error: error.message,
10
+ stack: error.stack,
11
+ pid: process.pid,
12
+ memory: process.memoryUsage(),
13
+ uptime: process.uptime(),
14
+ });
15
+ process.exit(1);
16
+ });
17
+
18
+ process.on('unhandledRejection', (reason, _promise) => {
19
+ logger.error('FATAL: Unhandled Promise Rejection', {
20
+ reason: reason instanceof Error ? reason.message : String(reason),
21
+ stack: reason instanceof Error ? reason.stack : undefined,
22
+ promise: '[Promise object]',
23
+ pid: process.pid,
24
+ memory: process.memoryUsage(),
25
+ uptime: process.uptime(),
26
+ });
27
+ process.exit(1);
28
+ });
29
+
30
+ async function main() {
31
+ logger.info('Starting MCP Rubber Duck Server', {
32
+ pid: process.pid,
33
+ nodeVersion: process.version,
34
+ platform: process.platform,
35
+ arch: process.arch,
36
+ cwd: process.cwd(),
37
+ argv: process.argv,
38
+ env: {
39
+ NODE_ENV: process.env.NODE_ENV,
40
+ LOG_LEVEL: process.env.LOG_LEVEL,
41
+ MCP_SERVER: process.env.MCP_SERVER,
42
+ MCP_BRIDGE_ENABLED: process.env.MCP_BRIDGE_ENABLED,
43
+ MCP_APPROVAL_MODE: process.env.MCP_APPROVAL_MODE,
44
+ }
45
+ });
46
+
47
+ try {
48
+ const server = new RubberDuckServer();
49
+
50
+ // Handle graceful shutdown
51
+ process.on('SIGINT', () => {
52
+ void (async () => {
53
+ logger.info('Received SIGINT, shutting down gracefully...', {
54
+ pid: process.pid,
55
+ uptime: process.uptime(),
56
+ });
57
+ try {
58
+ await server.stop();
59
+ logger.info('Server stopped gracefully');
60
+ process.exit(0);
61
+ } catch (error) {
62
+ logger.error('Error during graceful shutdown:', error);
63
+ process.exit(1);
64
+ }
65
+ })();
66
+ });
67
+
68
+ process.on('SIGTERM', () => {
69
+ void (async () => {
70
+ logger.info('Received SIGTERM, shutting down gracefully...', {
71
+ pid: process.pid,
72
+ uptime: process.uptime(),
73
+ });
74
+ try {
75
+ await server.stop();
76
+ logger.info('Server stopped gracefully');
77
+ process.exit(0);
78
+ } catch (error) {
79
+ logger.error('Error during graceful shutdown:', error);
80
+ process.exit(1);
81
+ }
82
+ })();
83
+ });
84
+
85
+ // Start the server
86
+ logger.info('Initializing server...');
87
+ await server.start();
88
+ logger.info('Server started successfully', {
89
+ pid: process.pid,
90
+ memory: process.memoryUsage(),
91
+ });
92
+ } catch (error) {
93
+ logger.error('Failed to start server:', {
94
+ error: error instanceof Error ? error.message : String(error),
95
+ stack: error instanceof Error ? error.stack : undefined,
96
+ pid: process.pid,
97
+ memory: process.memoryUsage(),
98
+ uptime: process.uptime(),
99
+ });
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ // Run the server
105
+ main().catch((error) => {
106
+ logger.error('Unhandled error in main:', {
107
+ error: error instanceof Error ? error.message : String(error),
108
+ stack: error instanceof Error ? error.stack : undefined,
109
+ pid: process.pid,
110
+ memory: process.memoryUsage(),
111
+ uptime: process.uptime(),
112
+ });
113
+ process.exit(1);
114
+ });