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.
- package/.dockerignore +19 -0
- package/.env.desktop.example +145 -0
- package/.env.example +45 -0
- package/.env.pi.example +106 -0
- package/.env.template +165 -0
- package/.eslintrc.json +40 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
- package/.github/ISSUE_TEMPLATE/question.md +67 -0
- package/.github/pull_request_template.md +111 -0
- package/.github/workflows/docker-build.yml +138 -0
- package/.github/workflows/release.yml +182 -0
- package/.github/workflows/security.yml +141 -0
- package/.github/workflows/semantic-release.yml +89 -0
- package/.prettierrc +10 -0
- package/.releaserc.json +66 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +242 -0
- package/Dockerfile +62 -0
- package/LICENSE +21 -0
- package/README.md +803 -0
- package/audit-ci.json +8 -0
- package/config/claude_desktop.json +14 -0
- package/config/config.example.json +91 -0
- package/dist/config/config.d.ts +51 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +301 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/types.d.ts +356 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +41 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/duck-provider-enhanced.d.ts +29 -0
- package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
- package/dist/providers/duck-provider-enhanced.js +230 -0
- package/dist/providers/duck-provider-enhanced.js.map +1 -0
- package/dist/providers/enhanced-manager.d.ts +54 -0
- package/dist/providers/enhanced-manager.d.ts.map +1 -0
- package/dist/providers/enhanced-manager.js +217 -0
- package/dist/providers/enhanced-manager.js.map +1 -0
- package/dist/providers/manager.d.ts +28 -0
- package/dist/providers/manager.d.ts.map +1 -0
- package/dist/providers/manager.js +204 -0
- package/dist/providers/manager.js.map +1 -0
- package/dist/providers/provider.d.ts +29 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +179 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/providers/types.d.ts +69 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +501 -0
- package/dist/server.js.map +1 -0
- package/dist/services/approval.d.ts +44 -0
- package/dist/services/approval.d.ts.map +1 -0
- package/dist/services/approval.js +159 -0
- package/dist/services/approval.js.map +1 -0
- package/dist/services/cache.d.ts +21 -0
- package/dist/services/cache.d.ts.map +1 -0
- package/dist/services/cache.js +63 -0
- package/dist/services/cache.js.map +1 -0
- package/dist/services/conversation.d.ts +24 -0
- package/dist/services/conversation.d.ts.map +1 -0
- package/dist/services/conversation.js +108 -0
- package/dist/services/conversation.js.map +1 -0
- package/dist/services/function-bridge.d.ts +41 -0
- package/dist/services/function-bridge.d.ts.map +1 -0
- package/dist/services/function-bridge.js +259 -0
- package/dist/services/function-bridge.js.map +1 -0
- package/dist/services/health.d.ts +17 -0
- package/dist/services/health.d.ts.map +1 -0
- package/dist/services/health.js +77 -0
- package/dist/services/health.js.map +1 -0
- package/dist/services/mcp-client-manager.d.ts +49 -0
- package/dist/services/mcp-client-manager.d.ts.map +1 -0
- package/dist/services/mcp-client-manager.js +279 -0
- package/dist/services/mcp-client-manager.js.map +1 -0
- package/dist/tools/approve-mcp-request.d.ts +9 -0
- package/dist/tools/approve-mcp-request.d.ts.map +1 -0
- package/dist/tools/approve-mcp-request.js +111 -0
- package/dist/tools/approve-mcp-request.js.map +1 -0
- package/dist/tools/ask-duck.d.ts +9 -0
- package/dist/tools/ask-duck.d.ts.map +1 -0
- package/dist/tools/ask-duck.js +43 -0
- package/dist/tools/ask-duck.js.map +1 -0
- package/dist/tools/chat-duck.d.ts +9 -0
- package/dist/tools/chat-duck.d.ts.map +1 -0
- package/dist/tools/chat-duck.js +57 -0
- package/dist/tools/chat-duck.js.map +1 -0
- package/dist/tools/clear-conversations.d.ts +8 -0
- package/dist/tools/clear-conversations.d.ts.map +1 -0
- package/dist/tools/clear-conversations.js +17 -0
- package/dist/tools/clear-conversations.js.map +1 -0
- package/dist/tools/compare-ducks.d.ts +8 -0
- package/dist/tools/compare-ducks.d.ts.map +1 -0
- package/dist/tools/compare-ducks.js +49 -0
- package/dist/tools/compare-ducks.js.map +1 -0
- package/dist/tools/duck-council.d.ts +8 -0
- package/dist/tools/duck-council.d.ts.map +1 -0
- package/dist/tools/duck-council.js +69 -0
- package/dist/tools/duck-council.js.map +1 -0
- package/dist/tools/get-pending-approvals.d.ts +15 -0
- package/dist/tools/get-pending-approvals.d.ts.map +1 -0
- package/dist/tools/get-pending-approvals.js +74 -0
- package/dist/tools/get-pending-approvals.js.map +1 -0
- package/dist/tools/list-ducks.d.ts +9 -0
- package/dist/tools/list-ducks.d.ts.map +1 -0
- package/dist/tools/list-ducks.js +47 -0
- package/dist/tools/list-ducks.js.map +1 -0
- package/dist/tools/list-models.d.ts +8 -0
- package/dist/tools/list-models.d.ts.map +1 -0
- package/dist/tools/list-models.js +72 -0
- package/dist/tools/list-models.js.map +1 -0
- package/dist/tools/mcp-status.d.ts +17 -0
- package/dist/tools/mcp-status.d.ts.map +1 -0
- package/dist/tools/mcp-status.js +100 -0
- package/dist/tools/mcp-status.js.map +1 -0
- package/dist/utils/ascii-art.d.ts +19 -0
- package/dist/utils/ascii-art.d.ts.map +1 -0
- package/dist/utils/ascii-art.js +73 -0
- package/dist/utils/ascii-art.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +86 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/safe-logger.d.ts +23 -0
- package/dist/utils/safe-logger.d.ts.map +1 -0
- package/dist/utils/safe-logger.js +145 -0
- package/dist/utils/safe-logger.js.map +1 -0
- package/docker-compose.yml +161 -0
- package/jest.config.js +26 -0
- package/package.json +65 -0
- package/scripts/build-multiarch.sh +290 -0
- package/scripts/deploy-raspbian.sh +410 -0
- package/scripts/deploy.sh +322 -0
- package/scripts/gh-deploy.sh +343 -0
- package/scripts/setup-docker-raspbian.sh +530 -0
- package/server.json +8 -0
- package/src/config/config.ts +357 -0
- package/src/config/types.ts +89 -0
- package/src/index.ts +114 -0
- package/src/providers/duck-provider-enhanced.ts +294 -0
- package/src/providers/enhanced-manager.ts +290 -0
- package/src/providers/manager.ts +257 -0
- package/src/providers/provider.ts +207 -0
- package/src/providers/types.ts +78 -0
- package/src/server.ts +603 -0
- package/src/services/approval.ts +225 -0
- package/src/services/cache.ts +79 -0
- package/src/services/conversation.ts +146 -0
- package/src/services/function-bridge.ts +329 -0
- package/src/services/health.ts +107 -0
- package/src/services/mcp-client-manager.ts +362 -0
- package/src/tools/approve-mcp-request.ts +126 -0
- package/src/tools/ask-duck.ts +74 -0
- package/src/tools/chat-duck.ts +82 -0
- package/src/tools/clear-conversations.ts +24 -0
- package/src/tools/compare-ducks.ts +67 -0
- package/src/tools/duck-council.ts +88 -0
- package/src/tools/get-pending-approvals.ts +90 -0
- package/src/tools/list-ducks.ts +65 -0
- package/src/tools/list-models.ts +101 -0
- package/src/tools/mcp-status.ts +117 -0
- package/src/utils/ascii-art.ts +85 -0
- package/src/utils/logger.ts +116 -0
- package/src/utils/safe-logger.ts +165 -0
- package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
- package/systemd/mcp-rubber-duck.service +58 -0
- package/test-functionality.js +147 -0
- package/test-mcp-interface.js +221 -0
- package/tests/ascii-art.test.ts +36 -0
- package/tests/config.test.ts +239 -0
- package/tests/conversation.test.ts +308 -0
- package/tests/mcp-bridge.test.ts +291 -0
- package/tests/providers.test.ts +269 -0
- package/tests/tools/clear-conversations.test.ts +163 -0
- 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
|
+
});
|