@dollhousemcp/mcp-server 1.6.2 → 1.6.4
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/CHANGELOG.md +22 -0
- package/README.md +28 -4
- package/dist/auth/GitHubAuthManager.d.ts.map +1 -1
- package/dist/auth/GitHubAuthManager.js +70 -12
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -12
- package/oauth-helper.mjs +363 -0
- package/package.json +2 -1
package/oauth-helper.mjs
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OAuth Helper Process - Standalone OAuth polling script
|
|
5
|
+
*
|
|
6
|
+
* This script runs independently of the MCP server to handle OAuth device flow polling.
|
|
7
|
+
* It's spawned as a detached process when authentication is initiated, polls GitHub
|
|
8
|
+
* for the OAuth token, stores it securely, and then exits.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node oauth-helper.mjs <device_code> <interval> <expires_in> <client_id>
|
|
11
|
+
*
|
|
12
|
+
* This solves the MCP server lifecycle issue where the server may shut down
|
|
13
|
+
* between tool calls, breaking background OAuth polling.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import fs from 'fs/promises';
|
|
19
|
+
import fsSync from 'fs';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
|
|
22
|
+
// Get the directory of this script
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
|
|
26
|
+
// Constants
|
|
27
|
+
const DEFAULT_POLL_INTERVAL = 5;
|
|
28
|
+
const DEFAULT_EXPIRES_IN = 900; // 15 minutes
|
|
29
|
+
const MAX_TOKEN_SIZE = 10000; // Maximum reasonable token size
|
|
30
|
+
|
|
31
|
+
// Parse command line arguments
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
if (args.length < 4) {
|
|
34
|
+
console.error('Usage: oauth-helper.mjs <device_code> <interval> <expires_in> <client_id>');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [deviceCode, intervalStr, expiresInStr, clientId] = args;
|
|
39
|
+
const pollInterval = parseInt(intervalStr, 10) || DEFAULT_POLL_INTERVAL;
|
|
40
|
+
const expiresIn = parseInt(expiresInStr, 10) || DEFAULT_EXPIRES_IN;
|
|
41
|
+
|
|
42
|
+
// Validate client ID is provided (no hardcoded fallback)
|
|
43
|
+
if (!clientId || clientId === 'undefined') {
|
|
44
|
+
console.error('OAUTH_HELPER_43: Missing or undefined client ID');
|
|
45
|
+
console.error('⚠️ GitHub OAuth Configuration Missing\n');
|
|
46
|
+
console.error('The server administrator needs to configure GitHub OAuth.');
|
|
47
|
+
console.error('Please contact your administrator to set up the DOLLHOUSE_GITHUB_CLIENT_ID.');
|
|
48
|
+
console.error('\nFor administrators: Set the environment variable before starting the server.');
|
|
49
|
+
await log('OAUTH_HELPER_43: Process exiting - missing client ID');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Log file for debugging (optional, can be disabled in production)
|
|
54
|
+
const LOG_FILE = join(homedir(), '.dollhouse', 'oauth-helper.log');
|
|
55
|
+
const LOG_ENABLED = process.env.DOLLHOUSE_OAUTH_DEBUG === 'true';
|
|
56
|
+
|
|
57
|
+
async function log(message) {
|
|
58
|
+
if (!LOG_ENABLED) return;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const timestamp = new Date().toISOString();
|
|
62
|
+
const logMessage = `[${timestamp}] ${message}\n`;
|
|
63
|
+
|
|
64
|
+
// Ensure directory exists with secure permissions
|
|
65
|
+
const logDir = dirname(LOG_FILE);
|
|
66
|
+
await fs.mkdir(logDir, { recursive: true, mode: 0o700 }).catch(() => {});
|
|
67
|
+
|
|
68
|
+
// Check if log file exists
|
|
69
|
+
let fileExists = false;
|
|
70
|
+
try {
|
|
71
|
+
await fs.access(LOG_FILE);
|
|
72
|
+
fileExists = true;
|
|
73
|
+
} catch {
|
|
74
|
+
fileExists = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Append to log file
|
|
78
|
+
await fs.appendFile(LOG_FILE, logMessage);
|
|
79
|
+
|
|
80
|
+
// Set secure permissions on first write
|
|
81
|
+
if (!fileExists) {
|
|
82
|
+
await fs.chmod(LOG_FILE, 0o600);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Silently fail if logging doesn't work
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function sleep(ms) {
|
|
90
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function pollGitHub(deviceCode, clientId) {
|
|
94
|
+
const TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(TOKEN_URL, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'Accept': 'application/json',
|
|
101
|
+
'Content-Type': 'application/json'
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
client_id: clientId,
|
|
105
|
+
device_code: deviceCode,
|
|
106
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
return data;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await log(`Network error polling GitHub: ${error.message}`);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function storeToken(token) {
|
|
119
|
+
// Validate token size to prevent DoS
|
|
120
|
+
if (!token || token.length > MAX_TOKEN_SIZE) {
|
|
121
|
+
await log('Invalid token size');
|
|
122
|
+
throw new Error('Invalid token received');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Import the compiled TokenManager
|
|
127
|
+
const { TokenManager } = await import('./dist/security/tokenManager.js');
|
|
128
|
+
|
|
129
|
+
// Store the token using the secure storage mechanism
|
|
130
|
+
await TokenManager.storeGitHubToken(token);
|
|
131
|
+
await log('Token stored successfully using TokenManager');
|
|
132
|
+
return true;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
await log(`Failed to store token using TokenManager: ${error.message}`);
|
|
135
|
+
|
|
136
|
+
// Fallback: Write to a temporary file for the MCP server to pick up
|
|
137
|
+
try {
|
|
138
|
+
const tempTokenFile = join(homedir(), '.dollhouse', '.auth', 'pending_token.txt');
|
|
139
|
+
const tempDir = dirname(tempTokenFile);
|
|
140
|
+
|
|
141
|
+
// Create directory with secure permissions
|
|
142
|
+
await fs.mkdir(tempDir, { recursive: true, mode: 0o700 });
|
|
143
|
+
|
|
144
|
+
// Verify directory permissions
|
|
145
|
+
const dirStats = await fs.stat(tempDir);
|
|
146
|
+
const dirMode = dirStats.mode & parseInt('777', 8);
|
|
147
|
+
if (dirMode !== parseInt('700', 8)) {
|
|
148
|
+
await fs.chmod(tempDir, 0o700);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Write token with secure permissions
|
|
152
|
+
await fs.writeFile(tempTokenFile, token, { mode: 0o600 });
|
|
153
|
+
|
|
154
|
+
// Verify file permissions
|
|
155
|
+
await fs.chmod(tempTokenFile, 0o600);
|
|
156
|
+
|
|
157
|
+
await log(`Token written to fallback file with secure permissions`);
|
|
158
|
+
return true;
|
|
159
|
+
} catch (fallbackError) {
|
|
160
|
+
await log(`Fallback storage also failed: ${fallbackError.message}`);
|
|
161
|
+
throw fallbackError;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cleanupPidFileSync() {
|
|
167
|
+
try {
|
|
168
|
+
const pidFile = join(homedir(), '.dollhouse', '.auth', 'oauth-helper.pid');
|
|
169
|
+
if (fsSync.existsSync(pidFile)) {
|
|
170
|
+
fsSync.unlinkSync(pidFile);
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Ignore cleanup errors
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function cleanupPidFile() {
|
|
178
|
+
try {
|
|
179
|
+
const pidFile = join(homedir(), '.dollhouse', '.auth', 'oauth-helper.pid');
|
|
180
|
+
await fs.unlink(pidFile).catch(() => {});
|
|
181
|
+
await log('PID file cleaned up');
|
|
182
|
+
} catch (error) {
|
|
183
|
+
// Ignore cleanup errors
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function writePidFile() {
|
|
188
|
+
try {
|
|
189
|
+
const pidFile = join(homedir(), '.dollhouse', '.auth', 'oauth-helper.pid');
|
|
190
|
+
const pidDir = dirname(pidFile);
|
|
191
|
+
|
|
192
|
+
await fs.mkdir(pidDir, { recursive: true, mode: 0o700 });
|
|
193
|
+
await fs.writeFile(pidFile, process.pid.toString(), { mode: 0o600 });
|
|
194
|
+
await log(`PID file written: ${pidFile}`);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
await log(`Failed to write PID file: ${error.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function main() {
|
|
201
|
+
await log(`[START] OAuth helper started - PID: ${process.pid}`);
|
|
202
|
+
await log(`[CONFIG] Device code: ${deviceCode.substring(0, 2)}****`); // More aggressive truncation
|
|
203
|
+
await log(`[CONFIG] Poll interval: ${pollInterval}s, Expires in: ${expiresIn}s`);
|
|
204
|
+
await log(`[CONFIG] Node version: ${process.version}`);
|
|
205
|
+
await log(`[CONFIG] Platform: ${process.platform}`);
|
|
206
|
+
// Never log client ID
|
|
207
|
+
|
|
208
|
+
// Write PID file for tracking
|
|
209
|
+
await writePidFile();
|
|
210
|
+
|
|
211
|
+
// Write initial heartbeat
|
|
212
|
+
let lastHeartbeat = Date.now();
|
|
213
|
+
const heartbeatInterval = setInterval(async () => {
|
|
214
|
+
await log(`[HEARTBEAT] Process alive - Memory: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`);
|
|
215
|
+
lastHeartbeat = Date.now();
|
|
216
|
+
}, 30000); // Every 30 seconds
|
|
217
|
+
|
|
218
|
+
const startTime = Date.now();
|
|
219
|
+
const timeout = startTime + (expiresIn * 1000);
|
|
220
|
+
let attempts = 0;
|
|
221
|
+
let consecutiveErrors = 0;
|
|
222
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
223
|
+
|
|
224
|
+
// Set up cleanup on exit - use synchronous cleanup for exit event
|
|
225
|
+
process.on('exit', () => {
|
|
226
|
+
cleanupPidFileSync();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Use beforeExit for async cleanup when possible
|
|
230
|
+
process.on('beforeExit', async () => {
|
|
231
|
+
await log('OAuth helper completing cleanup');
|
|
232
|
+
await cleanupPidFile();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
process.on('SIGINT', () => {
|
|
236
|
+
cleanupPidFileSync();
|
|
237
|
+
process.exit(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
process.on('SIGTERM', () => {
|
|
241
|
+
cleanupPidFileSync();
|
|
242
|
+
process.exit(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
while (Date.now() < timeout) {
|
|
246
|
+
attempts++;
|
|
247
|
+
const timeElapsed = Math.round((Date.now() - startTime) / 1000);
|
|
248
|
+
await log(`[POLL] Attempt ${attempts} at ${timeElapsed}s elapsed...`);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await pollGitHub(deviceCode, clientId);
|
|
252
|
+
|
|
253
|
+
if (response.error) {
|
|
254
|
+
switch (response.error) {
|
|
255
|
+
case 'authorization_pending':
|
|
256
|
+
// User hasn't authorized yet, keep polling
|
|
257
|
+
await log('[STATUS] Authorization pending, user has not authorized yet...');
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case 'slow_down':
|
|
261
|
+
// GitHub is asking us to slow down
|
|
262
|
+
await log(`[RATE_LIMIT] GitHub requested slower polling - increasing interval to ${pollInterval * 1.5}s`);
|
|
263
|
+
await sleep(pollInterval * 1500);
|
|
264
|
+
continue;
|
|
265
|
+
|
|
266
|
+
case 'expired_token':
|
|
267
|
+
await log('OAUTH_HELPER_264: Device code expired - authentication window closed');
|
|
268
|
+
console.error('OAUTH_EXPIRED: Device code expired at line 264 - authentication window closed');
|
|
269
|
+
clearInterval(heartbeatInterval);
|
|
270
|
+
await cleanupPidFile();
|
|
271
|
+
process.exit(1);
|
|
272
|
+
|
|
273
|
+
case 'access_denied':
|
|
274
|
+
await log('OAUTH_HELPER_270: User denied authorization request');
|
|
275
|
+
console.error('OAUTH_ACCESS_DENIED: User denied authorization at line 270');
|
|
276
|
+
clearInterval(heartbeatInterval);
|
|
277
|
+
await cleanupPidFile();
|
|
278
|
+
process.exit(1);
|
|
279
|
+
|
|
280
|
+
default:
|
|
281
|
+
await log(`OAUTH_HELPER_276: Unknown error from GitHub: ${response.error}`);
|
|
282
|
+
await log(`[ERROR] Error description: ${response.error_description}`);
|
|
283
|
+
console.error(`OAUTH_UNKNOWN_RESPONSE: Unknown error '${response.error}' at line 276`);
|
|
284
|
+
}
|
|
285
|
+
} else if (response.access_token) {
|
|
286
|
+
// Success! We got the token
|
|
287
|
+
await log('[SUCCESS] ✅ Token received from GitHub!');
|
|
288
|
+
consecutiveErrors = 0; // Reset error counter
|
|
289
|
+
|
|
290
|
+
// Store the token
|
|
291
|
+
const stored = await storeToken(response.access_token);
|
|
292
|
+
|
|
293
|
+
if (stored) {
|
|
294
|
+
await log('[SUCCESS] ✅ OAuth authentication completed successfully');
|
|
295
|
+
await log(`[STATS] Total attempts: ${attempts}, Time elapsed: ${Math.round((Date.now() - startTime) / 1000)}s`);
|
|
296
|
+
console.log('✅ GitHub authentication successful! Token has been stored.');
|
|
297
|
+
clearInterval(heartbeatInterval);
|
|
298
|
+
await cleanupPidFile();
|
|
299
|
+
process.exit(0);
|
|
300
|
+
} else {
|
|
301
|
+
await log('[ERROR] ❌ Failed to store token');
|
|
302
|
+
console.error('❌ Failed to store authentication token');
|
|
303
|
+
clearInterval(heartbeatInterval);
|
|
304
|
+
await cleanupPidFile();
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// Reset error counter on successful communication
|
|
309
|
+
consecutiveErrors = 0;
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
await log(`[ERROR] Polling error: ${error.message}`);
|
|
313
|
+
|
|
314
|
+
// Classify error types
|
|
315
|
+
const isNetworkError = error.message && (
|
|
316
|
+
error.message.includes('ECONNREFUSED') ||
|
|
317
|
+
error.message.includes('ETIMEDOUT') ||
|
|
318
|
+
error.message.includes('ENOTFOUND') ||
|
|
319
|
+
error.message.includes('EAI_AGAIN') ||
|
|
320
|
+
error.message.includes('fetch failed')
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (isNetworkError) {
|
|
324
|
+
consecutiveErrors++;
|
|
325
|
+
await log(`OAUTH_HELPER_319: Network error ${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}`);
|
|
326
|
+
|
|
327
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
328
|
+
await log('OAUTH_HELPER_323: Too many consecutive network errors, exiting');
|
|
329
|
+
console.error(`OAUTH_NETWORK_FAILURE: Too many network errors (${MAX_CONSECUTIVE_ERRORS}) at line 323 - check internet connection`);
|
|
330
|
+
clearInterval(heartbeatInterval);
|
|
331
|
+
await cleanupPidFile();
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
// Non-network error, likely fatal
|
|
336
|
+
await log(`OAUTH_HELPER_330: Non-recoverable error: ${error.message}`);
|
|
337
|
+
console.error(`OAUTH_FATAL_ERROR: Non-recoverable error at line 330 - ${error.message}`);
|
|
338
|
+
clearInterval(heartbeatInterval);
|
|
339
|
+
await cleanupPidFile();
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Wait before next poll
|
|
345
|
+
await sleep(pollInterval * 1000);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Timeout reached
|
|
349
|
+
await log('OAUTH_HELPER_342: OAuth authorization timed out');
|
|
350
|
+
await log(`[STATS] Total attempts: ${attempts}, Time elapsed: ${Math.round((Date.now() - startTime) / 1000)}s`);
|
|
351
|
+
console.error(`OAUTH_TIMEOUT: Authorization timed out at line 342 after ${Math.round((Date.now() - startTime) / 1000)}s - user did not authorize in time`);
|
|
352
|
+
clearInterval(heartbeatInterval);
|
|
353
|
+
await cleanupPidFile();
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Run the main function
|
|
358
|
+
main().catch(async (error) => {
|
|
359
|
+
await log(`Fatal error: ${error.message}`);
|
|
360
|
+
console.error('Fatal error in OAuth helper:', error);
|
|
361
|
+
await cleanupPidFile();
|
|
362
|
+
process.exit(1);
|
|
363
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dollhousemcp/mcp-server",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"data/ensembles/**/*.md",
|
|
95
95
|
"!dist/__tests__/**",
|
|
96
96
|
"!dist/**/*.test.*",
|
|
97
|
+
"oauth-helper.mjs",
|
|
97
98
|
"README.md",
|
|
98
99
|
"LICENSE",
|
|
99
100
|
"CHANGELOG.md"
|