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,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);