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,116 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
|
|
7
|
+
const logLevel = process.env.LOG_LEVEL || 'info';
|
|
8
|
+
const isMCP = process.env.MCP_SERVER === 'true' || process.argv.includes('--mcp');
|
|
9
|
+
|
|
10
|
+
// Determine logs directory based on environment and execution context
|
|
11
|
+
function getLogsDirectory(): string {
|
|
12
|
+
// Allow custom logs directory via environment variable
|
|
13
|
+
if (process.env.LOGS_DIR) {
|
|
14
|
+
return process.env.LOGS_DIR;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Try to use project directory when possible (development/direct execution)
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
const projectRoot = join(__dirname, '..', '..');
|
|
22
|
+
const projectLogsDir = join(projectRoot, 'logs');
|
|
23
|
+
|
|
24
|
+
// Check if we can write to project directory
|
|
25
|
+
if (existsSync(projectRoot)) {
|
|
26
|
+
return projectLogsDir;
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// Fall through to user directory if project root detection fails
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fallback to user directory for MCP server or when project root isn't writable
|
|
33
|
+
return join(homedir(), '.mcp-rubber-duck', 'logs');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Ensure logs directory exists
|
|
37
|
+
const logsDir = getLogsDirectory();
|
|
38
|
+
if (!existsSync(logsDir)) {
|
|
39
|
+
mkdirSync(logsDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Use simpler format for MCP to avoid interfering with JSON communication
|
|
43
|
+
const consoleFormat = isMCP
|
|
44
|
+
? winston.format.simple()
|
|
45
|
+
: winston.format.combine(
|
|
46
|
+
winston.format.colorize(),
|
|
47
|
+
winston.format.simple()
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// File format with more details for debugging crashes
|
|
51
|
+
const fileFormat = winston.format.combine(
|
|
52
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
53
|
+
winston.format.errors({ stack: true }),
|
|
54
|
+
winston.format.splat(),
|
|
55
|
+
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
|
|
56
|
+
let log = `${String(timestamp)} [${String(level).toUpperCase()}]: ${String(message)}`;
|
|
57
|
+
if (stack && typeof stack === 'string') {
|
|
58
|
+
log += `\nStack: ${stack}`;
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(meta).length > 0) {
|
|
61
|
+
log += `\nMeta: ${JSON.stringify(meta, null, 2)}`;
|
|
62
|
+
}
|
|
63
|
+
return log;
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
export const logger = winston.createLogger({
|
|
68
|
+
level: logLevel,
|
|
69
|
+
format: winston.format.combine(
|
|
70
|
+
winston.format.timestamp(),
|
|
71
|
+
winston.format.errors({ stack: true }),
|
|
72
|
+
winston.format.splat(),
|
|
73
|
+
winston.format.json()
|
|
74
|
+
),
|
|
75
|
+
transports: [
|
|
76
|
+
new winston.transports.Console({
|
|
77
|
+
format: consoleFormat,
|
|
78
|
+
silent: isMCP, // Always silence console logs in MCP mode to avoid interfering with JSON-RPC
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Always add file logging for better crash diagnosis
|
|
84
|
+
const filePrefix = isMCP ? 'mcp' : 'server';
|
|
85
|
+
|
|
86
|
+
// Error log
|
|
87
|
+
logger.add(
|
|
88
|
+
new winston.transports.File({
|
|
89
|
+
filename: join(logsDir, `${filePrefix}-error.log`),
|
|
90
|
+
level: 'error',
|
|
91
|
+
format: fileFormat,
|
|
92
|
+
maxsize: 10 * 1024 * 1024, // 10MB
|
|
93
|
+
maxFiles: 5,
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Combined log
|
|
98
|
+
logger.add(
|
|
99
|
+
new winston.transports.File({
|
|
100
|
+
filename: join(logsDir, `${filePrefix}-combined.log`),
|
|
101
|
+
format: fileFormat,
|
|
102
|
+
maxsize: 50 * 1024 * 1024, // 50MB
|
|
103
|
+
maxFiles: 3,
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Crash log for fatal errors
|
|
108
|
+
logger.add(
|
|
109
|
+
new winston.transports.File({
|
|
110
|
+
filename: join(logsDir, `${filePrefix}-crash.log`),
|
|
111
|
+
level: 'error',
|
|
112
|
+
format: fileFormat,
|
|
113
|
+
maxsize: 10 * 1024 * 1024, // 10MB
|
|
114
|
+
maxFiles: 10,
|
|
115
|
+
})
|
|
116
|
+
);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
|
|
3
|
+
// List of fields that should be sanitized in logs
|
|
4
|
+
const SENSITIVE_FIELDS = [
|
|
5
|
+
'password',
|
|
6
|
+
'apiKey',
|
|
7
|
+
'api_key',
|
|
8
|
+
'token',
|
|
9
|
+
'secret',
|
|
10
|
+
'auth',
|
|
11
|
+
'authorization',
|
|
12
|
+
'cookie',
|
|
13
|
+
'session',
|
|
14
|
+
'private_key',
|
|
15
|
+
'privateKey',
|
|
16
|
+
'client_secret',
|
|
17
|
+
'clientSecret',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// List of field patterns to look for
|
|
21
|
+
const SENSITIVE_PATTERNS = [
|
|
22
|
+
/password/i,
|
|
23
|
+
/secret/i,
|
|
24
|
+
/token/i,
|
|
25
|
+
/key$/i,
|
|
26
|
+
/auth/i,
|
|
27
|
+
/cookie/i,
|
|
28
|
+
/session/i,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sanitizes an object by redacting sensitive fields
|
|
33
|
+
*/
|
|
34
|
+
function sanitizeObject(obj: unknown, maxDepth = 5, currentDepth = 0): unknown {
|
|
35
|
+
if (currentDepth >= maxDepth) {
|
|
36
|
+
return '[Max depth exceeded]';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (obj === null || obj === undefined) {
|
|
40
|
+
return obj;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof obj === 'string') {
|
|
44
|
+
// Check if the string looks like a sensitive value (e.g., long random strings)
|
|
45
|
+
if (obj.length > 20 && /^[a-zA-Z0-9+/=]{20,}$/.test(obj)) {
|
|
46
|
+
return `[REDACTED:${obj.length}chars]`;
|
|
47
|
+
}
|
|
48
|
+
return obj;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
|
52
|
+
return obj;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(obj)) {
|
|
56
|
+
return obj.map((item: unknown) => sanitizeObject(item, maxDepth, currentDepth + 1));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof obj === 'object') {
|
|
60
|
+
const sanitized: Record<string, unknown> = {};
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
63
|
+
const keyLower = key.toLowerCase();
|
|
64
|
+
|
|
65
|
+
// Check if the field name is sensitive
|
|
66
|
+
const isSensitive = SENSITIVE_FIELDS.includes(keyLower) ||
|
|
67
|
+
SENSITIVE_PATTERNS.some(pattern => pattern.test(key));
|
|
68
|
+
|
|
69
|
+
if (isSensitive) {
|
|
70
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
71
|
+
sanitized[key] = `[REDACTED:${value.length}chars]`;
|
|
72
|
+
} else {
|
|
73
|
+
sanitized[key] = '[REDACTED]';
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
sanitized[key] = sanitizeObject(value, maxDepth, currentDepth + 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return sanitized;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return obj;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Safe logger that sanitizes sensitive data before logging
|
|
88
|
+
*/
|
|
89
|
+
export class SafeLogger {
|
|
90
|
+
static debug(message: string, data?: unknown): void {
|
|
91
|
+
if (data) {
|
|
92
|
+
const sanitizedData = sanitizeObject(data);
|
|
93
|
+
logger.debug(message, sanitizedData);
|
|
94
|
+
} else {
|
|
95
|
+
logger.debug(message);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static info(message: string, data?: unknown): void {
|
|
100
|
+
if (data) {
|
|
101
|
+
const sanitizedData = sanitizeObject(data);
|
|
102
|
+
logger.info(message, sanitizedData);
|
|
103
|
+
} else {
|
|
104
|
+
logger.info(message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static warn(message: string, data?: unknown): void {
|
|
109
|
+
if (data) {
|
|
110
|
+
const sanitizedData = sanitizeObject(data);
|
|
111
|
+
logger.warn(message, sanitizedData);
|
|
112
|
+
} else {
|
|
113
|
+
logger.warn(message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static error(message: string, data?: unknown): void {
|
|
118
|
+
if (data) {
|
|
119
|
+
const sanitizedData = sanitizeObject(data);
|
|
120
|
+
logger.error(message, sanitizedData);
|
|
121
|
+
} else {
|
|
122
|
+
logger.error(message);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Sanitize arguments object specifically for MCP tool calls
|
|
128
|
+
*/
|
|
129
|
+
static sanitizeToolArgs(args: unknown): unknown {
|
|
130
|
+
const sanitized = sanitizeObject(args);
|
|
131
|
+
|
|
132
|
+
// Additional sanitization for common patterns in tool arguments
|
|
133
|
+
if (typeof sanitized === 'object' && sanitized !== null) {
|
|
134
|
+
const objSanitized = sanitized as Record<string, unknown>;
|
|
135
|
+
// Sanitize file paths that might contain usernames
|
|
136
|
+
if (objSanitized.path && typeof objSanitized.path === 'string') {
|
|
137
|
+
objSanitized.path = objSanitized.path.replace(/\/Users\/[^/]+/, '/Users/[USER]');
|
|
138
|
+
objSanitized.path = (objSanitized.path as string).replace(/\\Users\\[^\\]+/, '\\Users\\[USER]');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Sanitize URLs that might contain credentials
|
|
142
|
+
if (objSanitized.url && typeof objSanitized.url === 'string') {
|
|
143
|
+
objSanitized.url = objSanitized.url.replace(
|
|
144
|
+
/(https?:\/\/)([^:]+):([^@]+)@/,
|
|
145
|
+
'$1[USER]:[REDACTED]@'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return sanitized;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create a safe message for approval requests
|
|
155
|
+
*/
|
|
156
|
+
static createApprovalMessage(duckName: string, server: string, tool: string, args: unknown): string {
|
|
157
|
+
const sanitizedArgs = this.sanitizeToolArgs(args);
|
|
158
|
+
const argsStr = JSON.stringify(sanitizedArgs, null, 2);
|
|
159
|
+
|
|
160
|
+
return `Duck "${duckName}" wants to call ${server}:${tool} with arguments:\n${argsStr}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Export the sanitize function for direct use
|
|
165
|
+
export { sanitizeObject };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=MCP Rubber Duck with Ollama - Multi-LLM AI Assistant
|
|
3
|
+
Documentation=https://github.com/nesquikm/mcp-rubber-duck
|
|
4
|
+
After=docker.service network-online.target
|
|
5
|
+
Wants=network-online.target
|
|
6
|
+
Requires=docker.service
|
|
7
|
+
|
|
8
|
+
[Service]
|
|
9
|
+
Type=forking
|
|
10
|
+
Restart=always
|
|
11
|
+
RestartSec=15
|
|
12
|
+
TimeoutStartSec=600
|
|
13
|
+
TimeoutStopSec=180
|
|
14
|
+
|
|
15
|
+
# User and group
|
|
16
|
+
User=pi
|
|
17
|
+
Group=pi
|
|
18
|
+
|
|
19
|
+
# Working directory (adjust path as needed)
|
|
20
|
+
WorkingDirectory=/home/pi/mcp-rubber-duck
|
|
21
|
+
|
|
22
|
+
# Environment
|
|
23
|
+
Environment=COMPOSE_PROJECT_NAME=mcp-rubber-duck
|
|
24
|
+
Environment=COMPOSE_FILE=docker-compose.yml
|
|
25
|
+
Environment=COMPOSE_PROFILES=with-ollama
|
|
26
|
+
|
|
27
|
+
# Commands
|
|
28
|
+
ExecStartPre=/usr/bin/docker compose -f ${COMPOSE_FILE} down
|
|
29
|
+
ExecStart=/usr/bin/docker compose -f ${COMPOSE_FILE} --profile with-ollama up -d
|
|
30
|
+
ExecStop=/usr/bin/docker compose -f ${COMPOSE_FILE} down
|
|
31
|
+
ExecReload=/usr/bin/docker compose -f ${COMPOSE_FILE} --profile with-ollama restart
|
|
32
|
+
|
|
33
|
+
# Health check (wait longer for Ollama to start)
|
|
34
|
+
ExecStartPost=/bin/sleep 60
|
|
35
|
+
ExecStartPost=/bin/sh -c 'docker inspect --format="{{.State.Health.Status}}" mcp-rubber-duck | grep -q healthy || exit 1'
|
|
36
|
+
|
|
37
|
+
# Logging
|
|
38
|
+
StandardOutput=journal
|
|
39
|
+
StandardError=journal
|
|
40
|
+
SyslogIdentifier=mcp-rubber-duck-ollama
|
|
41
|
+
|
|
42
|
+
# Security settings
|
|
43
|
+
NoNewPrivileges=yes
|
|
44
|
+
PrivateTmp=yes
|
|
45
|
+
PrivateDevices=yes
|
|
46
|
+
ProtectHome=yes
|
|
47
|
+
ProtectSystem=strict
|
|
48
|
+
ReadWritePaths=/home/pi/mcp-rubber-duck
|
|
49
|
+
|
|
50
|
+
# Resource limits (higher for Ollama)
|
|
51
|
+
MemoryMax=2G
|
|
52
|
+
CPUQuota=300%
|
|
53
|
+
|
|
54
|
+
[Install]
|
|
55
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=MCP Rubber Duck - Multi-LLM AI Assistant
|
|
3
|
+
Documentation=https://github.com/nesquikm/mcp-rubber-duck
|
|
4
|
+
After=docker.service network-online.target
|
|
5
|
+
Wants=network-online.target
|
|
6
|
+
Requires=docker.service
|
|
7
|
+
|
|
8
|
+
# If using Ollama, also wait for it
|
|
9
|
+
# After=docker.service network-online.target ollama.service
|
|
10
|
+
# Wants=network-online.target ollama.service
|
|
11
|
+
|
|
12
|
+
[Service]
|
|
13
|
+
Type=forking
|
|
14
|
+
Restart=always
|
|
15
|
+
RestartSec=10
|
|
16
|
+
TimeoutStartSec=300
|
|
17
|
+
TimeoutStopSec=120
|
|
18
|
+
|
|
19
|
+
# User and group
|
|
20
|
+
User=pi
|
|
21
|
+
Group=pi
|
|
22
|
+
|
|
23
|
+
# Working directory (adjust path as needed)
|
|
24
|
+
WorkingDirectory=/home/pi/mcp-rubber-duck
|
|
25
|
+
|
|
26
|
+
# Environment
|
|
27
|
+
Environment=COMPOSE_PROJECT_NAME=mcp-rubber-duck
|
|
28
|
+
Environment=COMPOSE_FILE=docker-compose.yml
|
|
29
|
+
|
|
30
|
+
# Commands
|
|
31
|
+
ExecStartPre=/usr/bin/docker compose -f ${COMPOSE_FILE} down
|
|
32
|
+
ExecStart=/usr/bin/docker compose -f ${COMPOSE_FILE} up -d
|
|
33
|
+
ExecStop=/usr/bin/docker compose -f ${COMPOSE_FILE} down
|
|
34
|
+
ExecReload=/usr/bin/docker compose -f ${COMPOSE_FILE} restart
|
|
35
|
+
|
|
36
|
+
# Health check
|
|
37
|
+
ExecStartPost=/bin/sleep 30
|
|
38
|
+
ExecStartPost=/bin/sh -c 'docker inspect --format="{{.State.Health.Status}}" mcp-rubber-duck | grep -q healthy || exit 1'
|
|
39
|
+
|
|
40
|
+
# Logging
|
|
41
|
+
StandardOutput=journal
|
|
42
|
+
StandardError=journal
|
|
43
|
+
SyslogIdentifier=mcp-rubber-duck
|
|
44
|
+
|
|
45
|
+
# Security settings
|
|
46
|
+
NoNewPrivileges=yes
|
|
47
|
+
PrivateTmp=yes
|
|
48
|
+
PrivateDevices=yes
|
|
49
|
+
ProtectHome=yes
|
|
50
|
+
ProtectSystem=strict
|
|
51
|
+
ReadWritePaths=/home/pi/mcp-rubber-duck
|
|
52
|
+
|
|
53
|
+
# Resource limits (adjust based on your Pi model)
|
|
54
|
+
MemoryMax=1G
|
|
55
|
+
CPUQuota=200%
|
|
56
|
+
|
|
57
|
+
[Install]
|
|
58
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Test script for MCP Rubber Duck functionality
|
|
4
|
+
import 'dotenv/config';
|
|
5
|
+
import { RubberDuckServer } from './dist/server.js';
|
|
6
|
+
import { ConfigManager } from './dist/config/config.js';
|
|
7
|
+
import { ProviderManager } from './dist/providers/manager.js';
|
|
8
|
+
import { ConversationManager } from './dist/services/conversation.js';
|
|
9
|
+
import { ResponseCache } from './dist/services/cache.js';
|
|
10
|
+
import { HealthMonitor } from './dist/services/health.js';
|
|
11
|
+
|
|
12
|
+
// Import tools
|
|
13
|
+
import { askDuckTool } from './dist/tools/ask-duck.js';
|
|
14
|
+
import { listDucksTool } from './dist/tools/list-ducks.js';
|
|
15
|
+
import { listModelsTool } from './dist/tools/list-models.js';
|
|
16
|
+
import { compareDucksTool } from './dist/tools/compare-ducks.js';
|
|
17
|
+
import { duckCouncilTool } from './dist/tools/duck-council.js';
|
|
18
|
+
import { chatDuckTool } from './dist/tools/chat-duck.js';
|
|
19
|
+
|
|
20
|
+
console.log('š¦ Testing MCP Rubber Duck Functionality\n');
|
|
21
|
+
console.log('API Keys loaded from .env:');
|
|
22
|
+
console.log(`- OpenAI: ${process.env.OPENAI_API_KEY ? 'ā
Found' : 'ā Missing'}`);
|
|
23
|
+
console.log(`- Gemini: ${process.env.GEMINI_API_KEY ? 'ā
Found' : 'ā Missing'}\n`);
|
|
24
|
+
|
|
25
|
+
async function runTests() {
|
|
26
|
+
try {
|
|
27
|
+
// Initialize managers
|
|
28
|
+
const configManager = new ConfigManager();
|
|
29
|
+
const providerManager = new ProviderManager(configManager);
|
|
30
|
+
const conversationManager = new ConversationManager();
|
|
31
|
+
const cache = new ResponseCache(300);
|
|
32
|
+
const healthMonitor = new HealthMonitor(providerManager);
|
|
33
|
+
|
|
34
|
+
// Test 1: List all ducks
|
|
35
|
+
console.log('š Test 1: List all ducks');
|
|
36
|
+
const ducksResult = await listDucksTool(providerManager, healthMonitor, { check_health: false });
|
|
37
|
+
console.log(ducksResult.content[0].text);
|
|
38
|
+
console.log('\n---\n');
|
|
39
|
+
|
|
40
|
+
// Test 2: Check health of all ducks
|
|
41
|
+
console.log('š„ Test 2: Health check');
|
|
42
|
+
await healthMonitor.performHealthChecks();
|
|
43
|
+
const healthyProviders = healthMonitor.getHealthyProviders();
|
|
44
|
+
console.log(`Healthy providers: ${healthyProviders.join(', ')}`);
|
|
45
|
+
console.log('\n---\n');
|
|
46
|
+
|
|
47
|
+
// Test 3: List models for all providers
|
|
48
|
+
console.log('š Test 3: List available models');
|
|
49
|
+
const modelsResult = await listModelsTool(providerManager, {});
|
|
50
|
+
console.log(modelsResult.content[0].text);
|
|
51
|
+
console.log('\n---\n');
|
|
52
|
+
|
|
53
|
+
// Test 4: Ask OpenAI
|
|
54
|
+
console.log('š¦ Test 4: Ask OpenAI');
|
|
55
|
+
try {
|
|
56
|
+
const openaiResult = await askDuckTool(providerManager, cache, {
|
|
57
|
+
prompt: 'What is 2+2? Answer in one word.',
|
|
58
|
+
provider: 'openai'
|
|
59
|
+
});
|
|
60
|
+
console.log(openaiResult.content[0].text);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`OpenAI error: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
console.log('\n---\n');
|
|
65
|
+
|
|
66
|
+
// Test 5: Ask Gemini
|
|
67
|
+
console.log('š¦ Test 5: Ask Gemini');
|
|
68
|
+
try {
|
|
69
|
+
const geminiResult = await askDuckTool(providerManager, cache, {
|
|
70
|
+
prompt: 'What is 3+3? Answer in one word.',
|
|
71
|
+
provider: 'gemini'
|
|
72
|
+
});
|
|
73
|
+
console.log(geminiResult.content[0].text);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`Gemini error: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('\n---\n');
|
|
78
|
+
|
|
79
|
+
// Test 6: Compare ducks
|
|
80
|
+
console.log('š Test 6: Compare ducks');
|
|
81
|
+
try {
|
|
82
|
+
const compareResult = await compareDucksTool(providerManager, cache, {
|
|
83
|
+
prompt: 'What is the capital of France? Answer in one word.'
|
|
84
|
+
});
|
|
85
|
+
console.log(compareResult.content[0].text);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`Compare error: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
console.log('\n---\n');
|
|
90
|
+
|
|
91
|
+
// Test 7: Duck council
|
|
92
|
+
console.log('šļø Test 7: Duck council');
|
|
93
|
+
try {
|
|
94
|
+
const councilResult = await duckCouncilTool(providerManager, {
|
|
95
|
+
prompt: 'What is the meaning of life? Answer in exactly 5 words.'
|
|
96
|
+
});
|
|
97
|
+
console.log(councilResult.content[0].text);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`Council error: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
console.log('\n---\n');
|
|
102
|
+
|
|
103
|
+
// Test 8: Chat with context
|
|
104
|
+
console.log('š¬ Test 8: Chat with context');
|
|
105
|
+
try {
|
|
106
|
+
// First message
|
|
107
|
+
await chatDuckTool(providerManager, conversationManager, {
|
|
108
|
+
conversation_id: 'test-chat',
|
|
109
|
+
message: 'My name is Alice.',
|
|
110
|
+
provider: 'openai'
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Second message using context
|
|
114
|
+
const chatResult = await chatDuckTool(providerManager, conversationManager, {
|
|
115
|
+
conversation_id: 'test-chat',
|
|
116
|
+
message: 'What is my name?'
|
|
117
|
+
});
|
|
118
|
+
console.log(chatResult.content[0].text);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`Chat error: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
console.log('\n---\n');
|
|
123
|
+
|
|
124
|
+
// Test 9: Test specific model
|
|
125
|
+
console.log('šÆ Test 9: Test specific model');
|
|
126
|
+
try {
|
|
127
|
+
const modelResult = await askDuckTool(providerManager, cache, {
|
|
128
|
+
prompt: 'Say hello',
|
|
129
|
+
provider: 'openai',
|
|
130
|
+
model: 'gpt-4o-mini'
|
|
131
|
+
});
|
|
132
|
+
console.log(modelResult.content[0].text);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`Model test error: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log('\nā
All tests completed!');
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Fatal error:', error);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Run tests
|
|
147
|
+
runTests().catch(console.error);
|