@mcp-z/client 2.2.3 → 2.2.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/dist/cjs/auth/oauth-callback-listener.js +2 -2
- package/dist/cjs/auth/oauth-callback-listener.js.map +1 -1
- package/dist/cjs/spawn/spawn-server.d.cts +2 -2
- package/dist/cjs/spawn/spawn-server.d.ts +2 -2
- package/dist/cjs/spawn/spawn-server.js +152 -29
- package/dist/cjs/spawn/spawn-server.js.map +1 -1
- package/dist/cjs/utils/path-utils.js +7 -4
- package/dist/cjs/utils/path-utils.js.map +1 -1
- package/dist/esm/auth/oauth-callback-listener.js +2 -2
- package/dist/esm/auth/oauth-callback-listener.js.map +1 -1
- package/dist/esm/spawn/spawn-server.d.ts +2 -2
- package/dist/esm/spawn/spawn-server.js +62 -27
- package/dist/esm/spawn/spawn-server.js.map +1 -1
- package/dist/esm/utils/path-utils.js +7 -4
- package/dist/esm/utils/path-utils.js.map +1 -1
- package/package.json +3 -1
|
@@ -188,7 +188,7 @@ var OAuthCallbackListener = /*#__PURE__*/ function() {
|
|
|
188
188
|
_this.server.on('error', function(error) {
|
|
189
189
|
reject(error);
|
|
190
190
|
});
|
|
191
|
-
_this.server.listen(port, function() {
|
|
191
|
+
_this.server.listen(port, 'localhost', function() {
|
|
192
192
|
resolve();
|
|
193
193
|
});
|
|
194
194
|
});
|
|
@@ -220,7 +220,7 @@ var OAuthCallbackListener = /*#__PURE__*/ function() {
|
|
|
220
220
|
res.writeHead(400, {
|
|
221
221
|
'Content-Type': 'text/html'
|
|
222
222
|
});
|
|
223
|
-
res.end("\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>
|
|
223
|
+
res.end("\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>The authorization provider returned an error.</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n ");
|
|
224
224
|
if (this.rejectCallback) {
|
|
225
225
|
this.rejectCallback(new Error(errorMessage));
|
|
226
226
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const iss = url.searchParams.get('iss');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, 'localhost', () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const iss = url.searchParams.get('iss');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>The authorization provider returned an error.</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error(errorMessage));\n }\n return;\n }\n\n // Validate code parameter\n if (!code) {\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Invalid Callback</h1>\n <p>Missing authorization code</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error('Missing authorization code'));\n }\n return;\n }\n\n // Success - send confirmation page\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(`\n <html>\n <head>\n <meta charset=\"UTF-8\">\n </head>\n <body>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n <script>setTimeout(() => window.close(), 2000);</script>\n </body>\n </html>\n `);\n\n // Resolve the promise with authorization code\n if (this.resolveCallback) {\n const result: CallbackResult = { code };\n if (state) {\n result.state = state;\n }\n if (iss) {\n result.iss = iss;\n }\n this.resolveCallback(result);\n }\n }\n\n /**\n * Wait for OAuth callback with timeout\n */\n async waitForCallback(timeoutMs = 300000): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n this.resolveCallback = resolve;\n this.rejectCallback = reject;\n\n // Set timeout to prevent hanging forever\n this.timeout = setTimeout(() => {\n reject(new Error(`Authorization timeout - no callback received within ${timeoutMs / 1000} seconds`));\n this.stop();\n }, timeoutMs);\n });\n }\n\n /**\n * Stop the callback server and close\n */\n async stop(): Promise<void> {\n // Clear the timeout\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n\n // Close the server\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server?.close(() => {\n this.logger.debug('🔒 Callback server closed');\n resolve();\n });\n });\n this.server = undefined;\n }\n }\n\n /**\n * Get the callback URL for this server\n */\n getCallbackUrl(): string {\n if (!this.port) {\n throw new Error('Server not started - call start() first');\n }\n return `http://localhost:${this.port}/callback`;\n }\n}\n"],"names":["OAuthCallbackListener","options","port","logger","defaultLogger","start","listen","debug","Promise","resolve","reject","server","http","createServer","req","res","handleRequest","on","error","url","URL","pathname","handleCallback","writeHead","end","code","searchParams","get","state","iss","errorDescription","errorMessage","rejectCallback","Error","resolveCallback","result","waitForCallback","timeoutMs","timeout","setTimeout","stop","clearTimeout","undefined","close","getCallbackUrl"],"mappings":"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"}
|
|
@@ -47,8 +47,8 @@ export interface ServerProcess {
|
|
|
47
47
|
*/
|
|
48
48
|
process: ChildProcess;
|
|
49
49
|
/**
|
|
50
|
-
* Close the server gracefully.
|
|
51
|
-
* Sends the specified signal (default: SIGINT), then
|
|
50
|
+
* Close the server gracefully, terminating its process tree on Windows.
|
|
51
|
+
* Sends the specified signal (default: SIGINT), then force-kills after timeout.
|
|
52
52
|
*
|
|
53
53
|
* @param signal - Signal to send (default: SIGINT)
|
|
54
54
|
* @param opts - Options including timeout
|
|
@@ -47,8 +47,8 @@ export interface ServerProcess {
|
|
|
47
47
|
*/
|
|
48
48
|
process: ChildProcess;
|
|
49
49
|
/**
|
|
50
|
-
* Close the server gracefully.
|
|
51
|
-
* Sends the specified signal (default: SIGINT), then
|
|
50
|
+
* Close the server gracefully, terminating its process tree on Windows.
|
|
51
|
+
* Sends the specified signal (default: SIGINT), then force-kills after timeout.
|
|
52
52
|
*
|
|
53
53
|
* @param signal - Signal to send (default: SIGINT)
|
|
54
54
|
* @param opts - Options including timeout
|
|
@@ -60,6 +60,12 @@ function _define_property(obj, key, value) {
|
|
|
60
60
|
} else obj[key] = value;
|
|
61
61
|
return obj;
|
|
62
62
|
}
|
|
63
|
+
function _instanceof(left, right) {
|
|
64
|
+
"@swc/helpers - instanceof";
|
|
65
|
+
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
|
|
66
|
+
return !!right[Symbol.hasInstance](left);
|
|
67
|
+
} else return left instanceof right;
|
|
68
|
+
}
|
|
63
69
|
function _getRequireWildcardCache(nodeInterop) {
|
|
64
70
|
if (typeof WeakMap !== "function") return null;
|
|
65
71
|
var cacheBabelInterop = new WeakMap();
|
|
@@ -271,6 +277,28 @@ function _unsupported_iterable_to_array(o, minLen) {
|
|
|
271
277
|
}
|
|
272
278
|
return result;
|
|
273
279
|
}
|
|
280
|
+
function terminateWindowsProcessTree(pid, force) {
|
|
281
|
+
var args = [
|
|
282
|
+
'/PID',
|
|
283
|
+
String(pid),
|
|
284
|
+
'/T'
|
|
285
|
+
];
|
|
286
|
+
if (force) args.push('/F');
|
|
287
|
+
return new Promise(function(resolve, reject) {
|
|
288
|
+
var taskkill = (0, _child_process.spawn)('taskkill.exe', args, {
|
|
289
|
+
stdio: 'ignore',
|
|
290
|
+
windowsHide: true
|
|
291
|
+
});
|
|
292
|
+
taskkill.once('error', reject);
|
|
293
|
+
taskkill.once('close', function(code) {
|
|
294
|
+
if (code === 0) {
|
|
295
|
+
resolve();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
reject(new Error("taskkill.exe failed to stop process tree rooted at PID ".concat(pid, " (exit code ").concat(code !== null && code !== void 0 ? code : 'unknown', ")")));
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
274
302
|
function spawnProcess(opts) {
|
|
275
303
|
var _opts_cwd, _opts_stdio, _opts_shell;
|
|
276
304
|
var name = opts.name;
|
|
@@ -332,55 +360,150 @@ function spawnProcess(opts) {
|
|
|
332
360
|
timeoutMs = (_opts_timeoutMs = _$opts.timeoutMs) !== null && _opts_timeoutMs !== void 0 ? _opts_timeoutMs : 500;
|
|
333
361
|
// Wait for 'close' event (process exit + stdio streams closed)
|
|
334
362
|
// This is better than 'exit' because it ensures stdio is fully cleaned up
|
|
335
|
-
closePromise = new Promise(function(resolve) {
|
|
363
|
+
closePromise = new Promise(function(resolve, reject) {
|
|
336
364
|
var isResolved = false;
|
|
337
365
|
var wasKilled = false;
|
|
338
|
-
var
|
|
366
|
+
var timedOut = false;
|
|
367
|
+
var resolveOnce = function resolveOnce(didTimeout) {
|
|
339
368
|
if (isResolved) return;
|
|
340
369
|
isResolved = true;
|
|
341
370
|
clearTimeout(timeout);
|
|
342
371
|
resolve({
|
|
343
|
-
timedOut:
|
|
372
|
+
timedOut: didTimeout,
|
|
344
373
|
killed: wasKilled
|
|
345
374
|
});
|
|
346
375
|
};
|
|
376
|
+
var rejectOnce = function rejectOnce(error) {
|
|
377
|
+
if (isResolved) return;
|
|
378
|
+
isResolved = true;
|
|
379
|
+
clearTimeout(timeout);
|
|
380
|
+
reject(error);
|
|
381
|
+
};
|
|
347
382
|
// Set timeout for forceful kill
|
|
348
383
|
var timeout = setTimeout(function() {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
384
|
+
timedOut = true;
|
|
385
|
+
var forceKill = function forceKill() {
|
|
386
|
+
return _async_to_generator(function() {
|
|
387
|
+
var error;
|
|
388
|
+
return _ts_generator(this, function(_state) {
|
|
389
|
+
switch(_state.label){
|
|
390
|
+
case 0:
|
|
391
|
+
_state.trys.push([
|
|
392
|
+
0,
|
|
393
|
+
4,
|
|
394
|
+
,
|
|
395
|
+
5
|
|
396
|
+
]);
|
|
397
|
+
if (!(_process.platform === 'win32' && child.pid)) return [
|
|
398
|
+
3,
|
|
399
|
+
2
|
|
400
|
+
];
|
|
401
|
+
return [
|
|
402
|
+
4,
|
|
403
|
+
terminateWindowsProcessTree(child.pid, true)
|
|
404
|
+
];
|
|
405
|
+
case 1:
|
|
406
|
+
_state.sent();
|
|
407
|
+
wasKilled = true;
|
|
408
|
+
return [
|
|
409
|
+
3,
|
|
410
|
+
3
|
|
411
|
+
];
|
|
412
|
+
case 2:
|
|
413
|
+
if (child.exitCode === null && !child.killed) {
|
|
414
|
+
wasKilled = child.kill('SIGKILL');
|
|
415
|
+
}
|
|
416
|
+
_state.label = 3;
|
|
417
|
+
case 3:
|
|
418
|
+
resolveOnce(true);
|
|
419
|
+
return [
|
|
420
|
+
3,
|
|
421
|
+
5
|
|
422
|
+
];
|
|
423
|
+
case 4:
|
|
424
|
+
error = _state.sent();
|
|
425
|
+
rejectOnce(_instanceof(error, Error) ? error : new Error(String(error)));
|
|
426
|
+
return [
|
|
427
|
+
3,
|
|
428
|
+
5
|
|
429
|
+
];
|
|
430
|
+
case 5:
|
|
431
|
+
return [
|
|
432
|
+
2
|
|
433
|
+
];
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
})();
|
|
437
|
+
};
|
|
438
|
+
void forceKill();
|
|
358
439
|
}, timeoutMs);
|
|
359
440
|
// Listen for 'close' event (not 'exit') to wait for stdio close
|
|
360
441
|
child.once('close', function() {
|
|
361
|
-
resolveOnce(false);
|
|
442
|
+
if (!timedOut) resolveOnce(false);
|
|
362
443
|
});
|
|
363
444
|
// Also listen for 'error' event in case spawn failed
|
|
364
445
|
// This prevents promise from hanging forever if process never started
|
|
365
446
|
child.once('error', function() {
|
|
366
447
|
resolveOnce(false);
|
|
367
448
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
449
|
+
var requestShutdown = function requestShutdown() {
|
|
450
|
+
return _async_to_generator(function() {
|
|
451
|
+
var error;
|
|
452
|
+
return _ts_generator(this, function(_state) {
|
|
453
|
+
switch(_state.label){
|
|
454
|
+
case 0:
|
|
455
|
+
_state.trys.push([
|
|
456
|
+
0,
|
|
457
|
+
4,
|
|
458
|
+
,
|
|
459
|
+
5
|
|
460
|
+
]);
|
|
461
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
462
|
+
resolveOnce(false);
|
|
463
|
+
return [
|
|
464
|
+
2
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
if (!(_process.platform === 'win32' && child.pid)) return [
|
|
468
|
+
3,
|
|
469
|
+
2
|
|
470
|
+
];
|
|
471
|
+
return [
|
|
472
|
+
4,
|
|
473
|
+
terminateWindowsProcessTree(child.pid, false)
|
|
474
|
+
];
|
|
475
|
+
case 1:
|
|
476
|
+
_state.sent();
|
|
477
|
+
return [
|
|
478
|
+
3,
|
|
479
|
+
3
|
|
480
|
+
];
|
|
481
|
+
case 2:
|
|
482
|
+
if (!child.kill(signal)) {
|
|
483
|
+
resolveOnce(false);
|
|
484
|
+
}
|
|
485
|
+
_state.label = 3;
|
|
486
|
+
case 3:
|
|
487
|
+
return [
|
|
488
|
+
3,
|
|
489
|
+
5
|
|
490
|
+
];
|
|
491
|
+
case 4:
|
|
492
|
+
error = _state.sent();
|
|
493
|
+
rejectOnce(_instanceof(error, Error) ? error : new Error(String(error)));
|
|
494
|
+
return [
|
|
495
|
+
3,
|
|
496
|
+
5
|
|
497
|
+
];
|
|
498
|
+
case 5:
|
|
499
|
+
return [
|
|
500
|
+
2
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
})();
|
|
505
|
+
};
|
|
506
|
+
void requestShutdown();
|
|
384
507
|
});
|
|
385
508
|
return [
|
|
386
509
|
2,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/spawn/spawn-server.ts"],"sourcesContent":["/**\n * Low-level single server spawning utilities.\n * Provides core process spawning with path resolution, environment management, and lifecycle control.\n */\n\nimport { type ChildProcess, type SpawnOptions, type StdioOptions, spawn } from 'child_process';\nimport * as process from 'process';\nimport { logger } from '../utils/logger.ts';\nimport { resolveArgsPaths } from '../utils/path-utils.ts';\n\n/**\n * Options for spawning a single server process.\n * @internal\n */\nexport interface SpawnProcessOptions {\n /** Server name for logging */\n name: string;\n /** Command to execute (e.g., 'node', 'npx') */\n command: string;\n /** Command arguments (paths will be resolved relative to cwd) */\n args?: string[];\n /** Working directory (must be absolute path) */\n cwd?: string;\n /** Additional environment variables (merged with process.env) */\n env?: Record<string, string>;\n /** Standard I/O configuration */\n stdio?: StdioOptions;\n /** Use shell for command execution (default: false, true on Windows) */\n shell?: boolean;\n}\n\n/**\n * Handle to a spawned server process.\n * Provides access to the process, resolved config, and lifecycle control.\n * @hidden\n */\nexport interface ServerProcess {\n /**\n * The resolved server configuration that was actually used.\n * Useful for debugging and understanding what was spawned.\n */\n config: {\n name: string;\n command: string;\n args: string[];\n cwd: string;\n env: Record<string, string>;\n stdio: StdioOptions;\n shell: boolean;\n };\n\n /**\n * The spawned child process.\n */\n process: ChildProcess;\n\n /**\n * Close the server gracefully.\n * Sends the specified signal (default: SIGINT), then SIGKILL after timeout.\n *\n * @param signal - Signal to send (default: SIGINT)\n * @param opts - Options including timeout\n * @returns Promise resolving to whether the process timed out and was force-killed\n */\n close: (signal?: NodeJS.Signals, opts?: { timeoutMs?: number }) => Promise<{ timedOut: boolean; killed: boolean }>;\n}\n\n/**\n * Normalize environment variables by merging with process.env and filtering undefined values.\n */\nfunction normalizeEnv(env?: Record<string, string>): Record<string, string> {\n const merged = { ...process.env, ...(env || {}) };\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(merged)) {\n if (value !== undefined) {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Spawn a single server process with path resolution and environment management.\n *\n * @internal\n * @param opts - Server spawn options\n * @returns ServerProcess handle with resolved config, process, and stop function\n *\n * @example\n * const handle = spawnProcess({\n * name: 'echo',\n * command: 'node',\n * args: ['./bin/server.js', '--port', '3000'],\n * cwd: '/home/user/project/test/lib/servers/echo',\n * env: { LOG_LEVEL: 'error' }\n * });\n *\n * // Later...\n * await handle.close();\n */\nexport function spawnProcess(opts: SpawnProcessOptions): ServerProcess {\n const name = opts.name;\n const command = opts.command;\n const cwd = opts.cwd ?? process.cwd();\n const stdio = opts.stdio ?? 'inherit';\n const shell = opts.shell ?? process.platform === 'win32';\n\n // Resolve paths in args relative to the working directory\n const args = opts.args ? resolveArgsPaths(opts.args, cwd) : [];\n\n // Merge environment variables\n const env = normalizeEnv(opts.env);\n\n // Create resolved config for return value\n const resolvedConfig = {\n name,\n command,\n args,\n cwd,\n env,\n stdio,\n shell,\n };\n\n // Log spawn operation\n logger.info(`[${name}] → ${command} ${args.join(' ')}`);\n\n // Spawn the process\n const spawnOpts: SpawnOptions = { cwd, env, stdio, shell };\n const child = spawn(command, args, spawnOpts);\n\n // Pipe stdio if not inherited\n if (child.stderr)\n child.stderr.on('data', (chunk) => {\n process.stderr.write(chunk);\n });\n\n // Attach lifecycle logging\n child.on('exit', (code, sig) => logger.info(`[${name}] exited (code=${code}, signal=${sig || 'none'})`));\n child.on('error', (err) => logger.info(`[${name}] process error: ${err.message}`));\n\n // Create stop function with graceful shutdown\n const stop = async (signal: NodeJS.Signals = 'SIGINT', opts: { timeoutMs?: number } = {}): Promise<{ timedOut: boolean; killed: boolean }> => {\n // If already exited, return immediately\n if (child.exitCode !== null || child.signalCode !== null) {\n return { timedOut: false, killed: false };\n }\n\n const timeoutMs = opts.timeoutMs ?? 500;\n\n // Wait for 'close' event (process exit + stdio streams closed)\n // This is better than 'exit' because it ensures stdio is fully cleaned up\n const closePromise = new Promise<{ timedOut: boolean; killed: boolean }>((resolve) => {\n let isResolved = false;\n let wasKilled = false;\n\n const resolveOnce = (timedOut: boolean) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n resolve({ timedOut, killed: wasKilled });\n };\n\n // Set timeout for forceful kill\n const timeout = setTimeout(() => {\n try {\n // Check again before SIGKILL\n if (child.exitCode === null && !child.killed) {\n child.kill('SIGKILL');\n wasKilled = true;\n }\n } catch (_) {}\n // Even if kill fails, resolve\n resolveOnce(true);\n }, timeoutMs);\n\n // Listen for 'close' event (not 'exit') to wait for stdio close\n child.once('close', () => {\n resolveOnce(false);\n });\n\n // Also listen for 'error' event in case spawn failed\n // This prevents promise from hanging forever if process never started\n child.once('error', () => {\n resolveOnce(false);\n });\n\n // Send graceful shutdown signal\n try {\n // Check one more time before killing\n if (child.exitCode !== null) {\n resolveOnce(false);\n return;\n }\n\n const killed = child.kill(signal);\n // If kill returned false, process already exited\n if (!killed) {\n resolveOnce(false);\n }\n } catch (_err) {\n // If kill throws, process is gone or unreachable\n resolveOnce(false);\n }\n });\n\n return closePromise;\n };\n\n return {\n config: resolvedConfig,\n process: child,\n close: stop,\n };\n}\n"],"names":["spawnProcess","normalizeEnv","env","merged","process","result","Object","entries","key","value","undefined","opts","name","command","cwd","stdio","shell","platform","args","resolveArgsPaths","resolvedConfig","logger","info","join","spawnOpts","child","spawn","stderr","on","chunk","write","code","sig","err","message","stop","signal","timeoutMs","closePromise","exitCode","signalCode","timedOut","killed","Promise","resolve","isResolved","wasKilled","resolveOnce","clearTimeout","timeout","setTimeout","kill","_","once","_err","config","close"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/spawn/spawn-server.ts"],"sourcesContent":["/**\n * Low-level single server spawning utilities.\n * Provides core process spawning with path resolution, environment management, and lifecycle control.\n */\n\nimport { type ChildProcess, type SpawnOptions, type StdioOptions, spawn } from 'child_process';\nimport * as process from 'process';\nimport { logger } from '../utils/logger.ts';\nimport { resolveArgsPaths } from '../utils/path-utils.ts';\n\n/**\n * Options for spawning a single server process.\n * @internal\n */\nexport interface SpawnProcessOptions {\n /** Server name for logging */\n name: string;\n /** Command to execute (e.g., 'node', 'npx') */\n command: string;\n /** Command arguments (paths will be resolved relative to cwd) */\n args?: string[];\n /** Working directory (must be absolute path) */\n cwd?: string;\n /** Additional environment variables (merged with process.env) */\n env?: Record<string, string>;\n /** Standard I/O configuration */\n stdio?: StdioOptions;\n /** Use shell for command execution (default: false, true on Windows) */\n shell?: boolean;\n}\n\n/**\n * Handle to a spawned server process.\n * Provides access to the process, resolved config, and lifecycle control.\n * @hidden\n */\nexport interface ServerProcess {\n /**\n * The resolved server configuration that was actually used.\n * Useful for debugging and understanding what was spawned.\n */\n config: {\n name: string;\n command: string;\n args: string[];\n cwd: string;\n env: Record<string, string>;\n stdio: StdioOptions;\n shell: boolean;\n };\n\n /**\n * The spawned child process.\n */\n process: ChildProcess;\n\n /**\n * Close the server gracefully, terminating its process tree on Windows.\n * Sends the specified signal (default: SIGINT), then force-kills after timeout.\n *\n * @param signal - Signal to send (default: SIGINT)\n * @param opts - Options including timeout\n * @returns Promise resolving to whether the process timed out and was force-killed\n */\n close: (signal?: NodeJS.Signals, opts?: { timeoutMs?: number }) => Promise<{ timedOut: boolean; killed: boolean }>;\n}\n\n/**\n * Normalize environment variables by merging with process.env and filtering undefined values.\n */\nfunction normalizeEnv(env?: Record<string, string>): Record<string, string> {\n const merged = { ...process.env, ...(env || {}) };\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(merged)) {\n if (value !== undefined) {\n result[key] = value;\n }\n }\n return result;\n}\n\nfunction terminateWindowsProcessTree(pid: number, force: boolean): Promise<void> {\n const args = ['/PID', String(pid), '/T'];\n if (force) args.push('/F');\n\n return new Promise((resolve, reject) => {\n const taskkill = spawn('taskkill.exe', args, { stdio: 'ignore', windowsHide: true });\n taskkill.once('error', reject);\n taskkill.once('close', (code) => {\n if (code === 0) {\n resolve();\n return;\n }\n reject(new Error(`taskkill.exe failed to stop process tree rooted at PID ${pid} (exit code ${code ?? 'unknown'})`));\n });\n });\n}\n\n/**\n * Spawn a single server process with path resolution and environment management.\n *\n * @internal\n * @param opts - Server spawn options\n * @returns ServerProcess handle with resolved config, process, and stop function\n *\n * @example\n * const handle = spawnProcess({\n * name: 'echo',\n * command: 'node',\n * args: ['./bin/server.js', '--port', '3000'],\n * cwd: '/home/user/project/test/lib/servers/echo',\n * env: { LOG_LEVEL: 'error' }\n * });\n *\n * // Later...\n * await handle.close();\n */\nexport function spawnProcess(opts: SpawnProcessOptions): ServerProcess {\n const name = opts.name;\n const command = opts.command;\n const cwd = opts.cwd ?? process.cwd();\n const stdio = opts.stdio ?? 'inherit';\n const shell = opts.shell ?? process.platform === 'win32';\n\n // Resolve paths in args relative to the working directory\n const args = opts.args ? resolveArgsPaths(opts.args, cwd) : [];\n\n // Merge environment variables\n const env = normalizeEnv(opts.env);\n\n // Create resolved config for return value\n const resolvedConfig = {\n name,\n command,\n args,\n cwd,\n env,\n stdio,\n shell,\n };\n\n // Log spawn operation\n logger.info(`[${name}] → ${command} ${args.join(' ')}`);\n\n // Spawn the process\n const spawnOpts: SpawnOptions = { cwd, env, stdio, shell };\n const child = spawn(command, args, spawnOpts);\n\n // Pipe stdio if not inherited\n if (child.stderr)\n child.stderr.on('data', (chunk) => {\n process.stderr.write(chunk);\n });\n\n // Attach lifecycle logging\n child.on('exit', (code, sig) => logger.info(`[${name}] exited (code=${code}, signal=${sig || 'none'})`));\n child.on('error', (err) => logger.info(`[${name}] process error: ${err.message}`));\n\n // Create stop function with graceful shutdown\n const stop = async (signal: NodeJS.Signals = 'SIGINT', opts: { timeoutMs?: number } = {}): Promise<{ timedOut: boolean; killed: boolean }> => {\n // If already exited, return immediately\n if (child.exitCode !== null || child.signalCode !== null) {\n return { timedOut: false, killed: false };\n }\n\n const timeoutMs = opts.timeoutMs ?? 500;\n\n // Wait for 'close' event (process exit + stdio streams closed)\n // This is better than 'exit' because it ensures stdio is fully cleaned up\n const closePromise = new Promise<{ timedOut: boolean; killed: boolean }>((resolve, reject) => {\n let isResolved = false;\n let wasKilled = false;\n let timedOut = false;\n\n const resolveOnce = (didTimeout: boolean) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n resolve({ timedOut: didTimeout, killed: wasKilled });\n };\n\n const rejectOnce = (error: Error) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n reject(error);\n };\n\n // Set timeout for forceful kill\n const timeout = setTimeout(() => {\n timedOut = true;\n const forceKill = async () => {\n try {\n if (process.platform === 'win32' && child.pid) {\n await terminateWindowsProcessTree(child.pid, true);\n wasKilled = true;\n } else if (child.exitCode === null && !child.killed) {\n wasKilled = child.kill('SIGKILL');\n }\n resolveOnce(true);\n } catch (error) {\n rejectOnce(error instanceof Error ? error : new Error(String(error)));\n }\n };\n void forceKill();\n }, timeoutMs);\n\n // Listen for 'close' event (not 'exit') to wait for stdio close\n child.once('close', () => {\n if (!timedOut) resolveOnce(false);\n });\n\n // Also listen for 'error' event in case spawn failed\n // This prevents promise from hanging forever if process never started\n child.once('error', () => {\n resolveOnce(false);\n });\n\n const requestShutdown = async () => {\n try {\n if (child.exitCode !== null || child.signalCode !== null) {\n resolveOnce(false);\n return;\n }\n\n if (process.platform === 'win32' && child.pid) {\n await terminateWindowsProcessTree(child.pid, false);\n } else if (!child.kill(signal)) {\n resolveOnce(false);\n }\n } catch (error) {\n rejectOnce(error instanceof Error ? error : new Error(String(error)));\n }\n };\n void requestShutdown();\n });\n\n return closePromise;\n };\n\n return {\n config: resolvedConfig,\n process: child,\n close: stop,\n };\n}\n"],"names":["spawnProcess","normalizeEnv","env","merged","process","result","Object","entries","key","value","undefined","terminateWindowsProcessTree","pid","force","args","String","push","Promise","resolve","reject","taskkill","spawn","stdio","windowsHide","once","code","Error","opts","name","command","cwd","shell","platform","resolveArgsPaths","resolvedConfig","logger","info","join","spawnOpts","child","stderr","on","chunk","write","sig","err","message","stop","signal","timeoutMs","closePromise","exitCode","signalCode","timedOut","killed","isResolved","wasKilled","resolveOnce","didTimeout","clearTimeout","timeout","rejectOnce","error","setTimeout","forceKill","kill","requestShutdown","config","close"],"mappings":"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"}
|
|
@@ -99,8 +99,11 @@ function _unsupported_iterable_to_array(o, minLen) {
|
|
|
99
99
|
}
|
|
100
100
|
function resolvePath(filePath, cwd) {
|
|
101
101
|
// Expand ~ to home directory
|
|
102
|
-
if (filePath === '~'
|
|
103
|
-
|
|
102
|
+
if (filePath === '~') {
|
|
103
|
+
return _os.homedir();
|
|
104
|
+
}
|
|
105
|
+
if (filePath.startsWith('~/')) {
|
|
106
|
+
filePath = _path.join(_os.homedir(), filePath.slice(2));
|
|
104
107
|
}
|
|
105
108
|
// If absolute, return as-is
|
|
106
109
|
if (_path.isAbsolute(filePath)) {
|
|
@@ -118,8 +121,8 @@ function resolveArgsPaths(args, cwd) {
|
|
|
118
121
|
var flagMatch = arg.match(/^(--.+?)=(.+)$/);
|
|
119
122
|
if (flagMatch) {
|
|
120
123
|
var _flagMatch = _sliced_to_array(flagMatch, 3), flag = _flagMatch[1], value = _flagMatch[2];
|
|
121
|
-
// Only resolve if the value looks like a path (contains
|
|
122
|
-
if (value && value.includes('/')) {
|
|
124
|
+
// Only resolve if the value looks like a path (contains a path separator).
|
|
125
|
+
if (value && (value.includes('/') || value.includes('\\'))) {
|
|
123
126
|
// Skip URLs in flag values
|
|
124
127
|
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value)) {
|
|
125
128
|
return arg;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/utils/path-utils.ts"],"sourcesContent":["/**\n * Path resolution utilities for cluster spawning.\n * Handles ~, relative paths, absolute paths, and special cases (URLs, npm packages, flags).\n */\n\nimport * as os from 'os';\nimport * as path from 'path';\n\n/**\n * Resolve a file path relative to a working directory.\n *\n * Handles three cases:\n * - `~` or `~/path` - Expands to home directory\n * - Absolute paths - Returns as-is\n * - Relative paths - Resolves relative to cwd\n *\n * @param filePath - The path to resolve\n * @param cwd - The working directory for resolving relative paths\n * @returns The resolved absolute path\n *\n * @example\n * resolvePath('~/config.json', '/unused') // → '/home/user/config.json'\n * resolvePath('/absolute/path', '/unused') // → '/absolute/path'\n * resolvePath('./relative', '/base') // → '/base/relative'\n */\nexport function resolvePath(filePath: string, cwd: string): string {\n // Expand ~ to home directory\n if (filePath === '~'
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/utils/path-utils.ts"],"sourcesContent":["/**\n * Path resolution utilities for cluster spawning.\n * Handles ~, relative paths, absolute paths, and special cases (URLs, npm packages, flags).\n */\n\nimport * as os from 'os';\nimport * as path from 'path';\n\n/**\n * Resolve a file path relative to a working directory.\n *\n * Handles three cases:\n * - `~` or `~/path` - Expands to home directory\n * - Absolute paths - Returns as-is\n * - Relative paths - Resolves relative to cwd\n *\n * @param filePath - The path to resolve\n * @param cwd - The working directory for resolving relative paths\n * @returns The resolved absolute path\n *\n * @example\n * resolvePath('~/config.json', '/unused') // → '/home/user/config.json'\n * resolvePath('/absolute/path', '/unused') // → '/absolute/path'\n * resolvePath('./relative', '/base') // → '/base/relative'\n */\nexport function resolvePath(filePath: string, cwd: string): string {\n // Expand ~ to home directory\n if (filePath === '~') {\n return os.homedir();\n }\n if (filePath.startsWith('~/')) {\n filePath = path.join(os.homedir(), filePath.slice(2));\n }\n\n // If absolute, return as-is\n if (path.isAbsolute(filePath)) {\n return filePath;\n }\n\n // Otherwise resolve relative to cwd\n return path.resolve(cwd, filePath);\n}\n\n/**\n * Resolve paths in command arguments array.\n *\n * Intelligently handles different argument types:\n * - Flags with paths: `--env-file=./path` → `--env-file=/absolute/path`\n * - Flags without paths: `--port=3000` → unchanged\n * - Command flags: `--verbose` → unchanged\n * - URLs: `http://example.com` → unchanged\n * - npm packages: `@scope/package` or `package-name` → unchanged\n * - Regular paths: `./script.js` → `/absolute/script.js`\n *\n * @param args - Array of command arguments\n * @param cwd - Working directory for resolving relative paths\n * @returns Array with paths resolved\n *\n * @example\n * resolveArgsPaths(['./bin/server.js', '--env-file=./config.env', '--port=3000'], '/home/user')\n * // → ['/home/user/bin/server.js', '--env-file=/home/user/config.env', '--port=3000']\n *\n * resolveArgsPaths(['@scope/package', 'https://example.com'], '/unused')\n * // → ['@scope/package', 'https://example.com'] (unchanged)\n */\nexport function resolveArgsPaths(args: string[], cwd: string): string[] {\n return args.map((arg) => {\n if (typeof arg !== 'string') {\n return arg;\n }\n\n // Check for flags with path values like --env-file=path, --config=path, etc.\n const flagMatch = arg.match(/^(--.+?)=(.+)$/);\n if (flagMatch) {\n const [, flag, value] = flagMatch;\n // Only resolve if the value looks like a path (contains a path separator).\n if (value && (value.includes('/') || value.includes('\\\\'))) {\n // Skip URLs in flag values\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(value)) {\n return arg;\n }\n return `${flag}=${resolvePath(value, cwd)}`;\n }\n return arg; // Return as-is for non-path flag values like --port=3000\n }\n\n // Skip command-line flags, only resolve actual paths\n if (arg.startsWith('-')) {\n return arg; // Don't resolve command-line flags as paths\n }\n\n // Skip URLs (http://, https://, etc.)\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(arg)) {\n return arg; // Return URLs as-is\n }\n\n // Skip npm package names (starting with @ or containing no path separators)\n if (arg.startsWith('@') || (!arg.includes('/') && !arg.includes('\\\\'))) {\n return arg; // Return npm package names as-is\n }\n\n // Regular arguments get resolved as paths\n return resolvePath(arg, cwd);\n });\n}\n"],"names":["resolveArgsPaths","resolvePath","filePath","cwd","os","homedir","startsWith","path","join","slice","isAbsolute","resolve","args","map","arg","flagMatch","match","flag","value","includes","test"],"mappings":"AAAA;;;CAGC;;;;;;;;;;;QA8DeA;eAAAA;;QAxCAC;eAAAA;;;0DApBI;4DACE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBf,SAASA,YAAYC,QAAgB,EAAEC,GAAW;IACvD,6BAA6B;IAC7B,IAAID,aAAa,KAAK;QACpB,OAAOE,IAAGC,OAAO;IACnB;IACA,IAAIH,SAASI,UAAU,CAAC,OAAO;QAC7BJ,WAAWK,MAAKC,IAAI,CAACJ,IAAGC,OAAO,IAAIH,SAASO,KAAK,CAAC;IACpD;IAEA,4BAA4B;IAC5B,IAAIF,MAAKG,UAAU,CAACR,WAAW;QAC7B,OAAOA;IACT;IAEA,oCAAoC;IACpC,OAAOK,MAAKI,OAAO,CAACR,KAAKD;AAC3B;AAwBO,SAASF,iBAAiBY,IAAc,EAAET,GAAW;IAC1D,OAAOS,KAAKC,GAAG,CAAC,SAACC;QACf,IAAI,OAAOA,QAAQ,UAAU;YAC3B,OAAOA;QACT;QAEA,6EAA6E;QAC7E,IAAMC,YAAYD,IAAIE,KAAK,CAAC;QAC5B,IAAID,WAAW;YACb,IAAwBA,8BAAAA,eAAfE,OAAeF,eAATG,QAASH;YACxB,2EAA2E;YAC3E,IAAIG,SAAUA,CAAAA,MAAMC,QAAQ,CAAC,QAAQD,MAAMC,QAAQ,CAAC,KAAI,GAAI;gBAC1D,2BAA2B;gBAC3B,IAAI,gCAAgCC,IAAI,CAACF,QAAQ;oBAC/C,OAAOJ;gBACT;gBACA,OAAO,AAAC,GAAUb,OAARgB,MAAK,KAA2B,OAAxBhB,YAAYiB,OAAOf;YACvC;YACA,OAAOW,KAAK,yDAAyD;QACvE;QAEA,qDAAqD;QACrD,IAAIA,IAAIR,UAAU,CAAC,MAAM;YACvB,OAAOQ,KAAK,4CAA4C;QAC1D;QAEA,sCAAsC;QACtC,IAAI,gCAAgCM,IAAI,CAACN,MAAM;YAC7C,OAAOA,KAAK,oBAAoB;QAClC;QAEA,4EAA4E;QAC5E,IAAIA,IAAIR,UAAU,CAAC,QAAS,CAACQ,IAAIK,QAAQ,CAAC,QAAQ,CAACL,IAAIK,QAAQ,CAAC,OAAQ;YACtE,OAAOL,KAAK,iCAAiC;QAC/C;QAEA,0CAA0C;QAC1C,OAAOb,YAAYa,KAAKX;IAC1B;AACF"}
|
|
@@ -26,7 +26,7 @@ import { logger as defaultLogger } from '../utils/logger.js';
|
|
|
26
26
|
this.server.on('error', (error)=>{
|
|
27
27
|
reject(error);
|
|
28
28
|
});
|
|
29
|
-
this.server.listen(port, ()=>{
|
|
29
|
+
this.server.listen(port, 'localhost', ()=>{
|
|
30
30
|
resolve();
|
|
31
31
|
});
|
|
32
32
|
});
|
|
@@ -62,7 +62,7 @@ import { logger as defaultLogger } from '../utils/logger.js';
|
|
|
62
62
|
<html>
|
|
63
63
|
<body>
|
|
64
64
|
<h1>Authorization Failed</h1>
|
|
65
|
-
<p
|
|
65
|
+
<p>The authorization provider returned an error.</p>
|
|
66
66
|
<script>setTimeout(() => window.close(), 3000);</script>
|
|
67
67
|
</body>
|
|
68
68
|
</html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const iss = url.searchParams.get('iss');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, 'localhost', () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const iss = url.searchParams.get('iss');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>The authorization provider returned an error.</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error(errorMessage));\n }\n return;\n }\n\n // Validate code parameter\n if (!code) {\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Invalid Callback</h1>\n <p>Missing authorization code</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error('Missing authorization code'));\n }\n return;\n }\n\n // Success - send confirmation page\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(`\n <html>\n <head>\n <meta charset=\"UTF-8\">\n </head>\n <body>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n <script>setTimeout(() => window.close(), 2000);</script>\n </body>\n </html>\n `);\n\n // Resolve the promise with authorization code\n if (this.resolveCallback) {\n const result: CallbackResult = { code };\n if (state) {\n result.state = state;\n }\n if (iss) {\n result.iss = iss;\n }\n this.resolveCallback(result);\n }\n }\n\n /**\n * Wait for OAuth callback with timeout\n */\n async waitForCallback(timeoutMs = 300000): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n this.resolveCallback = resolve;\n this.rejectCallback = reject;\n\n // Set timeout to prevent hanging forever\n this.timeout = setTimeout(() => {\n reject(new Error(`Authorization timeout - no callback received within ${timeoutMs / 1000} seconds`));\n this.stop();\n }, timeoutMs);\n });\n }\n\n /**\n * Stop the callback server and close\n */\n async stop(): Promise<void> {\n // Clear the timeout\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n\n // Close the server\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server?.close(() => {\n this.logger.debug('🔒 Callback server closed');\n resolve();\n });\n });\n this.server = undefined;\n }\n }\n\n /**\n * Get the callback URL for this server\n */\n getCallbackUrl(): string {\n if (!this.port) {\n throw new Error('Server not started - call start() first');\n }\n return `http://localhost:${this.port}/callback`;\n }\n}\n"],"names":["http","logger","defaultLogger","OAuthCallbackListener","start","listen","port","debug","Promise","resolve","reject","server","createServer","req","res","handleRequest","on","error","url","URL","pathname","handleCallback","writeHead","end","code","searchParams","get","state","iss","errorDescription","errorMessage","rejectCallback","Error","resolveCallback","result","waitForCallback","timeoutMs","timeout","setTimeout","stop","clearTimeout","undefined","close","getCallbackUrl","options"],"mappings":"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"}
|
|
@@ -47,8 +47,8 @@ export interface ServerProcess {
|
|
|
47
47
|
*/
|
|
48
48
|
process: ChildProcess;
|
|
49
49
|
/**
|
|
50
|
-
* Close the server gracefully.
|
|
51
|
-
* Sends the specified signal (default: SIGINT), then
|
|
50
|
+
* Close the server gracefully, terminating its process tree on Windows.
|
|
51
|
+
* Sends the specified signal (default: SIGINT), then force-kills after timeout.
|
|
52
52
|
*
|
|
53
53
|
* @param signal - Signal to send (default: SIGINT)
|
|
54
54
|
* @param opts - Options including timeout
|
|
@@ -20,6 +20,28 @@ import { resolveArgsPaths } from '../utils/path-utils.js';
|
|
|
20
20
|
}
|
|
21
21
|
return result;
|
|
22
22
|
}
|
|
23
|
+
function terminateWindowsProcessTree(pid, force) {
|
|
24
|
+
const args = [
|
|
25
|
+
'/PID',
|
|
26
|
+
String(pid),
|
|
27
|
+
'/T'
|
|
28
|
+
];
|
|
29
|
+
if (force) args.push('/F');
|
|
30
|
+
return new Promise((resolve, reject)=>{
|
|
31
|
+
const taskkill = spawn('taskkill.exe', args, {
|
|
32
|
+
stdio: 'ignore',
|
|
33
|
+
windowsHide: true
|
|
34
|
+
});
|
|
35
|
+
taskkill.once('error', reject);
|
|
36
|
+
taskkill.once('close', (code)=>{
|
|
37
|
+
if (code === 0) {
|
|
38
|
+
resolve();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
reject(new Error(`taskkill.exe failed to stop process tree rooted at PID ${pid} (exit code ${code !== null && code !== void 0 ? code : 'unknown'})`));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
23
45
|
/**
|
|
24
46
|
* Spawn a single server process with path resolution and environment management.
|
|
25
47
|
*
|
|
@@ -89,55 +111,68 @@ import { resolveArgsPaths } from '../utils/path-utils.js';
|
|
|
89
111
|
const timeoutMs = (_opts_timeoutMs = opts.timeoutMs) !== null && _opts_timeoutMs !== void 0 ? _opts_timeoutMs : 500;
|
|
90
112
|
// Wait for 'close' event (process exit + stdio streams closed)
|
|
91
113
|
// This is better than 'exit' because it ensures stdio is fully cleaned up
|
|
92
|
-
const closePromise = new Promise((resolve)=>{
|
|
114
|
+
const closePromise = new Promise((resolve, reject)=>{
|
|
93
115
|
let isResolved = false;
|
|
94
116
|
let wasKilled = false;
|
|
95
|
-
|
|
117
|
+
let timedOut = false;
|
|
118
|
+
const resolveOnce = (didTimeout)=>{
|
|
96
119
|
if (isResolved) return;
|
|
97
120
|
isResolved = true;
|
|
98
121
|
clearTimeout(timeout);
|
|
99
122
|
resolve({
|
|
100
|
-
timedOut,
|
|
123
|
+
timedOut: didTimeout,
|
|
101
124
|
killed: wasKilled
|
|
102
125
|
});
|
|
103
126
|
};
|
|
127
|
+
const rejectOnce = (error)=>{
|
|
128
|
+
if (isResolved) return;
|
|
129
|
+
isResolved = true;
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
reject(error);
|
|
132
|
+
};
|
|
104
133
|
// Set timeout for forceful kill
|
|
105
134
|
const timeout = setTimeout(()=>{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
timedOut = true;
|
|
136
|
+
const forceKill = async ()=>{
|
|
137
|
+
try {
|
|
138
|
+
if (process.platform === 'win32' && child.pid) {
|
|
139
|
+
await terminateWindowsProcessTree(child.pid, true);
|
|
140
|
+
wasKilled = true;
|
|
141
|
+
} else if (child.exitCode === null && !child.killed) {
|
|
142
|
+
wasKilled = child.kill('SIGKILL');
|
|
143
|
+
}
|
|
144
|
+
resolveOnce(true);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
rejectOnce(error instanceof Error ? error : new Error(String(error)));
|
|
111
147
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
resolveOnce(true);
|
|
148
|
+
};
|
|
149
|
+
void forceKill();
|
|
115
150
|
}, timeoutMs);
|
|
116
151
|
// Listen for 'close' event (not 'exit') to wait for stdio close
|
|
117
152
|
child.once('close', ()=>{
|
|
118
|
-
resolveOnce(false);
|
|
153
|
+
if (!timedOut) resolveOnce(false);
|
|
119
154
|
});
|
|
120
155
|
// Also listen for 'error' event in case spawn failed
|
|
121
156
|
// This prevents promise from hanging forever if process never started
|
|
122
157
|
child.once('error', ()=>{
|
|
123
158
|
resolveOnce(false);
|
|
124
159
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
const requestShutdown = async ()=>{
|
|
161
|
+
try {
|
|
162
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
163
|
+
resolveOnce(false);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (process.platform === 'win32' && child.pid) {
|
|
167
|
+
await terminateWindowsProcessTree(child.pid, false);
|
|
168
|
+
} else if (!child.kill(signal)) {
|
|
169
|
+
resolveOnce(false);
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
rejectOnce(error instanceof Error ? error : new Error(String(error)));
|
|
136
173
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
resolveOnce(false);
|
|
140
|
-
}
|
|
174
|
+
};
|
|
175
|
+
void requestShutdown();
|
|
141
176
|
});
|
|
142
177
|
return closePromise;
|
|
143
178
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/spawn/spawn-server.ts"],"sourcesContent":["/**\n * Low-level single server spawning utilities.\n * Provides core process spawning with path resolution, environment management, and lifecycle control.\n */\n\nimport { type ChildProcess, type SpawnOptions, type StdioOptions, spawn } from 'child_process';\nimport * as process from 'process';\nimport { logger } from '../utils/logger.ts';\nimport { resolveArgsPaths } from '../utils/path-utils.ts';\n\n/**\n * Options for spawning a single server process.\n * @internal\n */\nexport interface SpawnProcessOptions {\n /** Server name for logging */\n name: string;\n /** Command to execute (e.g., 'node', 'npx') */\n command: string;\n /** Command arguments (paths will be resolved relative to cwd) */\n args?: string[];\n /** Working directory (must be absolute path) */\n cwd?: string;\n /** Additional environment variables (merged with process.env) */\n env?: Record<string, string>;\n /** Standard I/O configuration */\n stdio?: StdioOptions;\n /** Use shell for command execution (default: false, true on Windows) */\n shell?: boolean;\n}\n\n/**\n * Handle to a spawned server process.\n * Provides access to the process, resolved config, and lifecycle control.\n * @hidden\n */\nexport interface ServerProcess {\n /**\n * The resolved server configuration that was actually used.\n * Useful for debugging and understanding what was spawned.\n */\n config: {\n name: string;\n command: string;\n args: string[];\n cwd: string;\n env: Record<string, string>;\n stdio: StdioOptions;\n shell: boolean;\n };\n\n /**\n * The spawned child process.\n */\n process: ChildProcess;\n\n /**\n * Close the server gracefully.\n * Sends the specified signal (default: SIGINT), then SIGKILL after timeout.\n *\n * @param signal - Signal to send (default: SIGINT)\n * @param opts - Options including timeout\n * @returns Promise resolving to whether the process timed out and was force-killed\n */\n close: (signal?: NodeJS.Signals, opts?: { timeoutMs?: number }) => Promise<{ timedOut: boolean; killed: boolean }>;\n}\n\n/**\n * Normalize environment variables by merging with process.env and filtering undefined values.\n */\nfunction normalizeEnv(env?: Record<string, string>): Record<string, string> {\n const merged = { ...process.env, ...(env || {}) };\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(merged)) {\n if (value !== undefined) {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Spawn a single server process with path resolution and environment management.\n *\n * @internal\n * @param opts - Server spawn options\n * @returns ServerProcess handle with resolved config, process, and stop function\n *\n * @example\n * const handle = spawnProcess({\n * name: 'echo',\n * command: 'node',\n * args: ['./bin/server.js', '--port', '3000'],\n * cwd: '/home/user/project/test/lib/servers/echo',\n * env: { LOG_LEVEL: 'error' }\n * });\n *\n * // Later...\n * await handle.close();\n */\nexport function spawnProcess(opts: SpawnProcessOptions): ServerProcess {\n const name = opts.name;\n const command = opts.command;\n const cwd = opts.cwd ?? process.cwd();\n const stdio = opts.stdio ?? 'inherit';\n const shell = opts.shell ?? process.platform === 'win32';\n\n // Resolve paths in args relative to the working directory\n const args = opts.args ? resolveArgsPaths(opts.args, cwd) : [];\n\n // Merge environment variables\n const env = normalizeEnv(opts.env);\n\n // Create resolved config for return value\n const resolvedConfig = {\n name,\n command,\n args,\n cwd,\n env,\n stdio,\n shell,\n };\n\n // Log spawn operation\n logger.info(`[${name}] → ${command} ${args.join(' ')}`);\n\n // Spawn the process\n const spawnOpts: SpawnOptions = { cwd, env, stdio, shell };\n const child = spawn(command, args, spawnOpts);\n\n // Pipe stdio if not inherited\n if (child.stderr)\n child.stderr.on('data', (chunk) => {\n process.stderr.write(chunk);\n });\n\n // Attach lifecycle logging\n child.on('exit', (code, sig) => logger.info(`[${name}] exited (code=${code}, signal=${sig || 'none'})`));\n child.on('error', (err) => logger.info(`[${name}] process error: ${err.message}`));\n\n // Create stop function with graceful shutdown\n const stop = async (signal: NodeJS.Signals = 'SIGINT', opts: { timeoutMs?: number } = {}): Promise<{ timedOut: boolean; killed: boolean }> => {\n // If already exited, return immediately\n if (child.exitCode !== null || child.signalCode !== null) {\n return { timedOut: false, killed: false };\n }\n\n const timeoutMs = opts.timeoutMs ?? 500;\n\n // Wait for 'close' event (process exit + stdio streams closed)\n // This is better than 'exit' because it ensures stdio is fully cleaned up\n const closePromise = new Promise<{ timedOut: boolean; killed: boolean }>((resolve) => {\n let isResolved = false;\n let wasKilled = false;\n\n const resolveOnce = (timedOut: boolean) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n resolve({ timedOut, killed: wasKilled });\n };\n\n // Set timeout for forceful kill\n const timeout = setTimeout(() => {\n try {\n // Check again before SIGKILL\n if (child.exitCode === null && !child.killed) {\n child.kill('SIGKILL');\n wasKilled = true;\n }\n } catch (_) {}\n // Even if kill fails, resolve\n resolveOnce(true);\n }, timeoutMs);\n\n // Listen for 'close' event (not 'exit') to wait for stdio close\n child.once('close', () => {\n resolveOnce(false);\n });\n\n // Also listen for 'error' event in case spawn failed\n // This prevents promise from hanging forever if process never started\n child.once('error', () => {\n resolveOnce(false);\n });\n\n // Send graceful shutdown signal\n try {\n // Check one more time before killing\n if (child.exitCode !== null) {\n resolveOnce(false);\n return;\n }\n\n const killed = child.kill(signal);\n // If kill returned false, process already exited\n if (!killed) {\n resolveOnce(false);\n }\n } catch (_err) {\n // If kill throws, process is gone or unreachable\n resolveOnce(false);\n }\n });\n\n return closePromise;\n };\n\n return {\n config: resolvedConfig,\n process: child,\n close: stop,\n };\n}\n"],"names":["spawn","process","logger","resolveArgsPaths","normalizeEnv","env","merged","result","key","value","Object","entries","undefined","spawnProcess","opts","name","command","cwd","stdio","shell","platform","args","resolvedConfig","info","join","spawnOpts","child","stderr","on","chunk","write","code","sig","err","message","stop","signal","exitCode","signalCode","timedOut","killed","timeoutMs","closePromise","Promise","resolve","isResolved","wasKilled","resolveOnce","clearTimeout","timeout","setTimeout","kill","_","once","_err","config","close"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/spawn/spawn-server.ts"],"sourcesContent":["/**\n * Low-level single server spawning utilities.\n * Provides core process spawning with path resolution, environment management, and lifecycle control.\n */\n\nimport { type ChildProcess, type SpawnOptions, type StdioOptions, spawn } from 'child_process';\nimport * as process from 'process';\nimport { logger } from '../utils/logger.ts';\nimport { resolveArgsPaths } from '../utils/path-utils.ts';\n\n/**\n * Options for spawning a single server process.\n * @internal\n */\nexport interface SpawnProcessOptions {\n /** Server name for logging */\n name: string;\n /** Command to execute (e.g., 'node', 'npx') */\n command: string;\n /** Command arguments (paths will be resolved relative to cwd) */\n args?: string[];\n /** Working directory (must be absolute path) */\n cwd?: string;\n /** Additional environment variables (merged with process.env) */\n env?: Record<string, string>;\n /** Standard I/O configuration */\n stdio?: StdioOptions;\n /** Use shell for command execution (default: false, true on Windows) */\n shell?: boolean;\n}\n\n/**\n * Handle to a spawned server process.\n * Provides access to the process, resolved config, and lifecycle control.\n * @hidden\n */\nexport interface ServerProcess {\n /**\n * The resolved server configuration that was actually used.\n * Useful for debugging and understanding what was spawned.\n */\n config: {\n name: string;\n command: string;\n args: string[];\n cwd: string;\n env: Record<string, string>;\n stdio: StdioOptions;\n shell: boolean;\n };\n\n /**\n * The spawned child process.\n */\n process: ChildProcess;\n\n /**\n * Close the server gracefully, terminating its process tree on Windows.\n * Sends the specified signal (default: SIGINT), then force-kills after timeout.\n *\n * @param signal - Signal to send (default: SIGINT)\n * @param opts - Options including timeout\n * @returns Promise resolving to whether the process timed out and was force-killed\n */\n close: (signal?: NodeJS.Signals, opts?: { timeoutMs?: number }) => Promise<{ timedOut: boolean; killed: boolean }>;\n}\n\n/**\n * Normalize environment variables by merging with process.env and filtering undefined values.\n */\nfunction normalizeEnv(env?: Record<string, string>): Record<string, string> {\n const merged = { ...process.env, ...(env || {}) };\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(merged)) {\n if (value !== undefined) {\n result[key] = value;\n }\n }\n return result;\n}\n\nfunction terminateWindowsProcessTree(pid: number, force: boolean): Promise<void> {\n const args = ['/PID', String(pid), '/T'];\n if (force) args.push('/F');\n\n return new Promise((resolve, reject) => {\n const taskkill = spawn('taskkill.exe', args, { stdio: 'ignore', windowsHide: true });\n taskkill.once('error', reject);\n taskkill.once('close', (code) => {\n if (code === 0) {\n resolve();\n return;\n }\n reject(new Error(`taskkill.exe failed to stop process tree rooted at PID ${pid} (exit code ${code ?? 'unknown'})`));\n });\n });\n}\n\n/**\n * Spawn a single server process with path resolution and environment management.\n *\n * @internal\n * @param opts - Server spawn options\n * @returns ServerProcess handle with resolved config, process, and stop function\n *\n * @example\n * const handle = spawnProcess({\n * name: 'echo',\n * command: 'node',\n * args: ['./bin/server.js', '--port', '3000'],\n * cwd: '/home/user/project/test/lib/servers/echo',\n * env: { LOG_LEVEL: 'error' }\n * });\n *\n * // Later...\n * await handle.close();\n */\nexport function spawnProcess(opts: SpawnProcessOptions): ServerProcess {\n const name = opts.name;\n const command = opts.command;\n const cwd = opts.cwd ?? process.cwd();\n const stdio = opts.stdio ?? 'inherit';\n const shell = opts.shell ?? process.platform === 'win32';\n\n // Resolve paths in args relative to the working directory\n const args = opts.args ? resolveArgsPaths(opts.args, cwd) : [];\n\n // Merge environment variables\n const env = normalizeEnv(opts.env);\n\n // Create resolved config for return value\n const resolvedConfig = {\n name,\n command,\n args,\n cwd,\n env,\n stdio,\n shell,\n };\n\n // Log spawn operation\n logger.info(`[${name}] → ${command} ${args.join(' ')}`);\n\n // Spawn the process\n const spawnOpts: SpawnOptions = { cwd, env, stdio, shell };\n const child = spawn(command, args, spawnOpts);\n\n // Pipe stdio if not inherited\n if (child.stderr)\n child.stderr.on('data', (chunk) => {\n process.stderr.write(chunk);\n });\n\n // Attach lifecycle logging\n child.on('exit', (code, sig) => logger.info(`[${name}] exited (code=${code}, signal=${sig || 'none'})`));\n child.on('error', (err) => logger.info(`[${name}] process error: ${err.message}`));\n\n // Create stop function with graceful shutdown\n const stop = async (signal: NodeJS.Signals = 'SIGINT', opts: { timeoutMs?: number } = {}): Promise<{ timedOut: boolean; killed: boolean }> => {\n // If already exited, return immediately\n if (child.exitCode !== null || child.signalCode !== null) {\n return { timedOut: false, killed: false };\n }\n\n const timeoutMs = opts.timeoutMs ?? 500;\n\n // Wait for 'close' event (process exit + stdio streams closed)\n // This is better than 'exit' because it ensures stdio is fully cleaned up\n const closePromise = new Promise<{ timedOut: boolean; killed: boolean }>((resolve, reject) => {\n let isResolved = false;\n let wasKilled = false;\n let timedOut = false;\n\n const resolveOnce = (didTimeout: boolean) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n resolve({ timedOut: didTimeout, killed: wasKilled });\n };\n\n const rejectOnce = (error: Error) => {\n if (isResolved) return;\n isResolved = true;\n clearTimeout(timeout);\n reject(error);\n };\n\n // Set timeout for forceful kill\n const timeout = setTimeout(() => {\n timedOut = true;\n const forceKill = async () => {\n try {\n if (process.platform === 'win32' && child.pid) {\n await terminateWindowsProcessTree(child.pid, true);\n wasKilled = true;\n } else if (child.exitCode === null && !child.killed) {\n wasKilled = child.kill('SIGKILL');\n }\n resolveOnce(true);\n } catch (error) {\n rejectOnce(error instanceof Error ? error : new Error(String(error)));\n }\n };\n void forceKill();\n }, timeoutMs);\n\n // Listen for 'close' event (not 'exit') to wait for stdio close\n child.once('close', () => {\n if (!timedOut) resolveOnce(false);\n });\n\n // Also listen for 'error' event in case spawn failed\n // This prevents promise from hanging forever if process never started\n child.once('error', () => {\n resolveOnce(false);\n });\n\n const requestShutdown = async () => {\n try {\n if (child.exitCode !== null || child.signalCode !== null) {\n resolveOnce(false);\n return;\n }\n\n if (process.platform === 'win32' && child.pid) {\n await terminateWindowsProcessTree(child.pid, false);\n } else if (!child.kill(signal)) {\n resolveOnce(false);\n }\n } catch (error) {\n rejectOnce(error instanceof Error ? error : new Error(String(error)));\n }\n };\n void requestShutdown();\n });\n\n return closePromise;\n };\n\n return {\n config: resolvedConfig,\n process: child,\n close: stop,\n };\n}\n"],"names":["spawn","process","logger","resolveArgsPaths","normalizeEnv","env","merged","result","key","value","Object","entries","undefined","terminateWindowsProcessTree","pid","force","args","String","push","Promise","resolve","reject","taskkill","stdio","windowsHide","once","code","Error","spawnProcess","opts","name","command","cwd","shell","platform","resolvedConfig","info","join","spawnOpts","child","stderr","on","chunk","write","sig","err","message","stop","signal","exitCode","signalCode","timedOut","killed","timeoutMs","closePromise","isResolved","wasKilled","resolveOnce","didTimeout","clearTimeout","timeout","rejectOnce","error","setTimeout","forceKill","kill","requestShutdown","config","close"],"mappings":"AAAA;;;CAGC,GAED,SAAkEA,KAAK,QAAQ,gBAAgB;AAC/F,YAAYC,aAAa,UAAU;AACnC,SAASC,MAAM,QAAQ,qBAAqB;AAC5C,SAASC,gBAAgB,QAAQ,yBAAyB;AA2D1D;;CAEC,GACD,SAASC,aAAaC,GAA4B;IAChD,MAAMC,SAAS;QAAE,GAAGL,QAAQI,GAAG;QAAE,GAAIA,OAAO,CAAC,CAAC;IAAE;IAChD,MAAME,SAAiC,CAAC;IACxC,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACL,QAAS;QACjD,IAAIG,UAAUG,WAAW;YACvBL,MAAM,CAACC,IAAI,GAAGC;QAChB;IACF;IACA,OAAOF;AACT;AAEA,SAASM,4BAA4BC,GAAW,EAAEC,KAAc;IAC9D,MAAMC,OAAO;QAAC;QAAQC,OAAOH;QAAM;KAAK;IACxC,IAAIC,OAAOC,KAAKE,IAAI,CAAC;IAErB,OAAO,IAAIC,QAAQ,CAACC,SAASC;QAC3B,MAAMC,WAAWtB,MAAM,gBAAgBgB,MAAM;YAAEO,OAAO;YAAUC,aAAa;QAAK;QAClFF,SAASG,IAAI,CAAC,SAASJ;QACvBC,SAASG,IAAI,CAAC,SAAS,CAACC;YACtB,IAAIA,SAAS,GAAG;gBACdN;gBACA;YACF;YACAC,OAAO,IAAIM,MAAM,CAAC,uDAAuD,EAAEb,IAAI,YAAY,EAAEY,iBAAAA,kBAAAA,OAAQ,UAAU,CAAC,CAAC;QACnH;IACF;AACF;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASE,aAAaC,IAAyB;QAGxCA,WACEA,aACAA;IAJd,MAAMC,OAAOD,KAAKC,IAAI;IACtB,MAAMC,UAAUF,KAAKE,OAAO;IAC5B,MAAMC,OAAMH,YAAAA,KAAKG,GAAG,cAARH,uBAAAA,YAAY5B,QAAQ+B,GAAG;IACnC,MAAMT,SAAQM,cAAAA,KAAKN,KAAK,cAAVM,yBAAAA,cAAc;IAC5B,MAAMI,SAAQJ,cAAAA,KAAKI,KAAK,cAAVJ,yBAAAA,cAAc5B,QAAQiC,QAAQ,KAAK;IAEjD,0DAA0D;IAC1D,MAAMlB,OAAOa,KAAKb,IAAI,GAAGb,iBAAiB0B,KAAKb,IAAI,EAAEgB,OAAO,EAAE;IAE9D,8BAA8B;IAC9B,MAAM3B,MAAMD,aAAayB,KAAKxB,GAAG;IAEjC,0CAA0C;IAC1C,MAAM8B,iBAAiB;QACrBL;QACAC;QACAf;QACAgB;QACA3B;QACAkB;QACAU;IACF;IAEA,sBAAsB;IACtB/B,OAAOkC,IAAI,CAAC,CAAC,CAAC,EAAEN,KAAK,IAAI,EAAEC,QAAQ,CAAC,EAAEf,KAAKqB,IAAI,CAAC,MAAM;IAEtD,oBAAoB;IACpB,MAAMC,YAA0B;QAAEN;QAAK3B;QAAKkB;QAAOU;IAAM;IACzD,MAAMM,QAAQvC,MAAM+B,SAASf,MAAMsB;IAEnC,8BAA8B;IAC9B,IAAIC,MAAMC,MAAM,EACdD,MAAMC,MAAM,CAACC,EAAE,CAAC,QAAQ,CAACC;QACvBzC,QAAQuC,MAAM,CAACG,KAAK,CAACD;IACvB;IAEF,2BAA2B;IAC3BH,MAAME,EAAE,CAAC,QAAQ,CAACf,MAAMkB,MAAQ1C,OAAOkC,IAAI,CAAC,CAAC,CAAC,EAAEN,KAAK,eAAe,EAAEJ,KAAK,SAAS,EAAEkB,OAAO,OAAO,CAAC,CAAC;IACtGL,MAAME,EAAE,CAAC,SAAS,CAACI,MAAQ3C,OAAOkC,IAAI,CAAC,CAAC,CAAC,EAAEN,KAAK,iBAAiB,EAAEe,IAAIC,OAAO,EAAE;IAEhF,8CAA8C;IAC9C,MAAMC,OAAO,OAAOC,SAAyB,QAAQ,EAAEnB,OAA+B,CAAC,CAAC;YAMpEA;QALlB,wCAAwC;QACxC,IAAIU,MAAMU,QAAQ,KAAK,QAAQV,MAAMW,UAAU,KAAK,MAAM;YACxD,OAAO;gBAAEC,UAAU;gBAAOC,QAAQ;YAAM;QAC1C;QAEA,MAAMC,aAAYxB,kBAAAA,KAAKwB,SAAS,cAAdxB,6BAAAA,kBAAkB;QAEpC,+DAA+D;QAC/D,0EAA0E;QAC1E,MAAMyB,eAAe,IAAInC,QAAgD,CAACC,SAASC;YACjF,IAAIkC,aAAa;YACjB,IAAIC,YAAY;YAChB,IAAIL,WAAW;YAEf,MAAMM,cAAc,CAACC;gBACnB,IAAIH,YAAY;gBAChBA,aAAa;gBACbI,aAAaC;gBACbxC,QAAQ;oBAAE+B,UAAUO;oBAAYN,QAAQI;gBAAU;YACpD;YAEA,MAAMK,aAAa,CAACC;gBAClB,IAAIP,YAAY;gBAChBA,aAAa;gBACbI,aAAaC;gBACbvC,OAAOyC;YACT;YAEA,gCAAgC;YAChC,MAAMF,UAAUG,WAAW;gBACzBZ,WAAW;gBACX,MAAMa,YAAY;oBAChB,IAAI;wBACF,IAAI/D,QAAQiC,QAAQ,KAAK,WAAWK,MAAMzB,GAAG,EAAE;4BAC7C,MAAMD,4BAA4B0B,MAAMzB,GAAG,EAAE;4BAC7C0C,YAAY;wBACd,OAAO,IAAIjB,MAAMU,QAAQ,KAAK,QAAQ,CAACV,MAAMa,MAAM,EAAE;4BACnDI,YAAYjB,MAAM0B,IAAI,CAAC;wBACzB;wBACAR,YAAY;oBACd,EAAE,OAAOK,OAAO;wBACdD,WAAWC,iBAAiBnC,QAAQmC,QAAQ,IAAInC,MAAMV,OAAO6C;oBAC/D;gBACF;gBACA,KAAKE;YACP,GAAGX;YAEH,gEAAgE;YAChEd,MAAMd,IAAI,CAAC,SAAS;gBAClB,IAAI,CAAC0B,UAAUM,YAAY;YAC7B;YAEA,qDAAqD;YACrD,sEAAsE;YACtElB,MAAMd,IAAI,CAAC,SAAS;gBAClBgC,YAAY;YACd;YAEA,MAAMS,kBAAkB;gBACtB,IAAI;oBACF,IAAI3B,MAAMU,QAAQ,KAAK,QAAQV,MAAMW,UAAU,KAAK,MAAM;wBACxDO,YAAY;wBACZ;oBACF;oBAEA,IAAIxD,QAAQiC,QAAQ,KAAK,WAAWK,MAAMzB,GAAG,EAAE;wBAC7C,MAAMD,4BAA4B0B,MAAMzB,GAAG,EAAE;oBAC/C,OAAO,IAAI,CAACyB,MAAM0B,IAAI,CAACjB,SAAS;wBAC9BS,YAAY;oBACd;gBACF,EAAE,OAAOK,OAAO;oBACdD,WAAWC,iBAAiBnC,QAAQmC,QAAQ,IAAInC,MAAMV,OAAO6C;gBAC/D;YACF;YACA,KAAKI;QACP;QAEA,OAAOZ;IACT;IAEA,OAAO;QACLa,QAAQhC;QACRlC,SAASsC;QACT6B,OAAOrB;IACT;AACF"}
|
|
@@ -21,8 +21,11 @@ import * as path from 'path';
|
|
|
21
21
|
* resolvePath('./relative', '/base') // → '/base/relative'
|
|
22
22
|
*/ export function resolvePath(filePath, cwd) {
|
|
23
23
|
// Expand ~ to home directory
|
|
24
|
-
if (filePath === '~'
|
|
25
|
-
|
|
24
|
+
if (filePath === '~') {
|
|
25
|
+
return os.homedir();
|
|
26
|
+
}
|
|
27
|
+
if (filePath.startsWith('~/')) {
|
|
28
|
+
filePath = path.join(os.homedir(), filePath.slice(2));
|
|
26
29
|
}
|
|
27
30
|
// If absolute, return as-is
|
|
28
31
|
if (path.isAbsolute(filePath)) {
|
|
@@ -61,8 +64,8 @@ import * as path from 'path';
|
|
|
61
64
|
const flagMatch = arg.match(/^(--.+?)=(.+)$/);
|
|
62
65
|
if (flagMatch) {
|
|
63
66
|
const [, flag, value] = flagMatch;
|
|
64
|
-
// Only resolve if the value looks like a path (contains
|
|
65
|
-
if (value && value.includes('/')) {
|
|
67
|
+
// Only resolve if the value looks like a path (contains a path separator).
|
|
68
|
+
if (value && (value.includes('/') || value.includes('\\'))) {
|
|
66
69
|
// Skip URLs in flag values
|
|
67
70
|
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value)) {
|
|
68
71
|
return arg;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/utils/path-utils.ts"],"sourcesContent":["/**\n * Path resolution utilities for cluster spawning.\n * Handles ~, relative paths, absolute paths, and special cases (URLs, npm packages, flags).\n */\n\nimport * as os from 'os';\nimport * as path from 'path';\n\n/**\n * Resolve a file path relative to a working directory.\n *\n * Handles three cases:\n * - `~` or `~/path` - Expands to home directory\n * - Absolute paths - Returns as-is\n * - Relative paths - Resolves relative to cwd\n *\n * @param filePath - The path to resolve\n * @param cwd - The working directory for resolving relative paths\n * @returns The resolved absolute path\n *\n * @example\n * resolvePath('~/config.json', '/unused') // → '/home/user/config.json'\n * resolvePath('/absolute/path', '/unused') // → '/absolute/path'\n * resolvePath('./relative', '/base') // → '/base/relative'\n */\nexport function resolvePath(filePath: string, cwd: string): string {\n // Expand ~ to home directory\n if (filePath === '~'
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/utils/path-utils.ts"],"sourcesContent":["/**\n * Path resolution utilities for cluster spawning.\n * Handles ~, relative paths, absolute paths, and special cases (URLs, npm packages, flags).\n */\n\nimport * as os from 'os';\nimport * as path from 'path';\n\n/**\n * Resolve a file path relative to a working directory.\n *\n * Handles three cases:\n * - `~` or `~/path` - Expands to home directory\n * - Absolute paths - Returns as-is\n * - Relative paths - Resolves relative to cwd\n *\n * @param filePath - The path to resolve\n * @param cwd - The working directory for resolving relative paths\n * @returns The resolved absolute path\n *\n * @example\n * resolvePath('~/config.json', '/unused') // → '/home/user/config.json'\n * resolvePath('/absolute/path', '/unused') // → '/absolute/path'\n * resolvePath('./relative', '/base') // → '/base/relative'\n */\nexport function resolvePath(filePath: string, cwd: string): string {\n // Expand ~ to home directory\n if (filePath === '~') {\n return os.homedir();\n }\n if (filePath.startsWith('~/')) {\n filePath = path.join(os.homedir(), filePath.slice(2));\n }\n\n // If absolute, return as-is\n if (path.isAbsolute(filePath)) {\n return filePath;\n }\n\n // Otherwise resolve relative to cwd\n return path.resolve(cwd, filePath);\n}\n\n/**\n * Resolve paths in command arguments array.\n *\n * Intelligently handles different argument types:\n * - Flags with paths: `--env-file=./path` → `--env-file=/absolute/path`\n * - Flags without paths: `--port=3000` → unchanged\n * - Command flags: `--verbose` → unchanged\n * - URLs: `http://example.com` → unchanged\n * - npm packages: `@scope/package` or `package-name` → unchanged\n * - Regular paths: `./script.js` → `/absolute/script.js`\n *\n * @param args - Array of command arguments\n * @param cwd - Working directory for resolving relative paths\n * @returns Array with paths resolved\n *\n * @example\n * resolveArgsPaths(['./bin/server.js', '--env-file=./config.env', '--port=3000'], '/home/user')\n * // → ['/home/user/bin/server.js', '--env-file=/home/user/config.env', '--port=3000']\n *\n * resolveArgsPaths(['@scope/package', 'https://example.com'], '/unused')\n * // → ['@scope/package', 'https://example.com'] (unchanged)\n */\nexport function resolveArgsPaths(args: string[], cwd: string): string[] {\n return args.map((arg) => {\n if (typeof arg !== 'string') {\n return arg;\n }\n\n // Check for flags with path values like --env-file=path, --config=path, etc.\n const flagMatch = arg.match(/^(--.+?)=(.+)$/);\n if (flagMatch) {\n const [, flag, value] = flagMatch;\n // Only resolve if the value looks like a path (contains a path separator).\n if (value && (value.includes('/') || value.includes('\\\\'))) {\n // Skip URLs in flag values\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(value)) {\n return arg;\n }\n return `${flag}=${resolvePath(value, cwd)}`;\n }\n return arg; // Return as-is for non-path flag values like --port=3000\n }\n\n // Skip command-line flags, only resolve actual paths\n if (arg.startsWith('-')) {\n return arg; // Don't resolve command-line flags as paths\n }\n\n // Skip URLs (http://, https://, etc.)\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(arg)) {\n return arg; // Return URLs as-is\n }\n\n // Skip npm package names (starting with @ or containing no path separators)\n if (arg.startsWith('@') || (!arg.includes('/') && !arg.includes('\\\\'))) {\n return arg; // Return npm package names as-is\n }\n\n // Regular arguments get resolved as paths\n return resolvePath(arg, cwd);\n });\n}\n"],"names":["os","path","resolvePath","filePath","cwd","homedir","startsWith","join","slice","isAbsolute","resolve","resolveArgsPaths","args","map","arg","flagMatch","match","flag","value","includes","test"],"mappings":"AAAA;;;CAGC,GAED,YAAYA,QAAQ,KAAK;AACzB,YAAYC,UAAU,OAAO;AAE7B;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,YAAYC,QAAgB,EAAEC,GAAW;IACvD,6BAA6B;IAC7B,IAAID,aAAa,KAAK;QACpB,OAAOH,GAAGK,OAAO;IACnB;IACA,IAAIF,SAASG,UAAU,CAAC,OAAO;QAC7BH,WAAWF,KAAKM,IAAI,CAACP,GAAGK,OAAO,IAAIF,SAASK,KAAK,CAAC;IACpD;IAEA,4BAA4B;IAC5B,IAAIP,KAAKQ,UAAU,CAACN,WAAW;QAC7B,OAAOA;IACT;IAEA,oCAAoC;IACpC,OAAOF,KAAKS,OAAO,CAACN,KAAKD;AAC3B;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASQ,iBAAiBC,IAAc,EAAER,GAAW;IAC1D,OAAOQ,KAAKC,GAAG,CAAC,CAACC;QACf,IAAI,OAAOA,QAAQ,UAAU;YAC3B,OAAOA;QACT;QAEA,6EAA6E;QAC7E,MAAMC,YAAYD,IAAIE,KAAK,CAAC;QAC5B,IAAID,WAAW;YACb,MAAM,GAAGE,MAAMC,MAAM,GAAGH;YACxB,2EAA2E;YAC3E,IAAIG,SAAUA,CAAAA,MAAMC,QAAQ,CAAC,QAAQD,MAAMC,QAAQ,CAAC,KAAI,GAAI;gBAC1D,2BAA2B;gBAC3B,IAAI,gCAAgCC,IAAI,CAACF,QAAQ;oBAC/C,OAAOJ;gBACT;gBACA,OAAO,GAAGG,KAAK,CAAC,EAAEf,YAAYgB,OAAOd,MAAM;YAC7C;YACA,OAAOU,KAAK,yDAAyD;QACvE;QAEA,qDAAqD;QACrD,IAAIA,IAAIR,UAAU,CAAC,MAAM;YACvB,OAAOQ,KAAK,4CAA4C;QAC1D;QAEA,sCAAsC;QACtC,IAAI,gCAAgCM,IAAI,CAACN,MAAM;YAC7C,OAAOA,KAAK,oBAAoB;QAClC;QAEA,4EAA4E;QAC5E,IAAIA,IAAIR,UAAU,CAAC,QAAS,CAACQ,IAAIK,QAAQ,CAAC,QAAQ,CAACL,IAAIK,QAAQ,CAAC,OAAQ;YACtE,OAAOL,KAAK,iCAAiC;QAC/C;QAEA,0CAA0C;QAC1C,OAAOZ,YAAYY,KAAKV;IAC1B;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-z/client",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "Programmatic MCP client library for Node.js - connect, discover, and call tools on Model Context Protocol servers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
"prepublish:check": "ncp",
|
|
55
55
|
"prepublishOnly": "node scripts/require-dist-tag.cjs && tsds validate",
|
|
56
56
|
"test": "tsds test:node --no-timeouts",
|
|
57
|
+
"test:ci": "tsds test:node --no-timeouts --ignore \"test/integration/auth/todoist-oauth.test.ts\"",
|
|
58
|
+
"test:ci:engines": "nvu engines tsds test:node --no-timeouts --ignore \"test/integration/auth/todoist-oauth.test.ts\"",
|
|
57
59
|
"test:engines": "nvu engines tsds test:node --no-timeouts",
|
|
58
60
|
"version": "tsds version"
|
|
59
61
|
},
|