@mcp-z/client 2.2.2 → 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/README.md CHANGED
@@ -11,33 +11,42 @@ Programmatic MCP client library for Node.js - connect, discover, and call tools
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npm install --save-dev @mcp-z/client
14
+ npm install @mcp-z/client
15
15
  ```
16
16
 
17
- Requires Node.js >= 22.
17
+ Requires Node.js >= 20.
18
18
 
19
- ## Agent skill
19
+ ## Quick start
20
20
 
21
- Install the repository's `mcp-z-client` skill globally when an agent will write code that consumes this package:
21
+ Create a local stdio server that exposes an `echo` tool:
22
22
 
23
23
  ```bash
24
- npx skills add https://github.com/mcp-z/client.git -g -s mcp-z-client
24
+ npm install @modelcontextprotocol/sdk zod
25
25
  ```
26
26
 
27
- ## Quick start
27
+ Save this as `echo-server.mjs`:
28
28
 
29
- ```ts
30
- import { createServerRegistry } from '@mcp-z/client';
29
+ ```js
30
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
31
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
32
+ import { z } from 'zod';
31
33
 
32
- const registry = createServerRegistry({
33
- todoist: { type: 'http', url: 'https://ai.todoist.net/mcp' }
34
- });
34
+ const server = new McpServer({ name: 'echo', version: '1.0.0' });
35
+ server.registerTool('echo', { inputSchema: { message: z.string() } }, async ({ message }) => ({
36
+ content: [{ type: 'text', text: message }]
37
+ }));
38
+ await server.connect(new StdioServerTransport());
39
+ ```
35
40
 
36
- const client = await registry.connect('todoist');
37
- await client.callTool('add-tasks', {
38
- tasks: [{ content: 'Learn MCP', priority: 4 }]
39
- });
41
+ Connect to it and print the returned text:
40
42
 
43
+ ```ts
44
+ import { createServerRegistry } from '@mcp-z/client';
45
+
46
+ const registry = createServerRegistry({ echo: { command: 'node', args: ['echo-server.mjs'] } });
47
+ const client = await registry.connect('echo');
48
+ const response = await client.callTool('echo', { message: 'hello MCP' });
49
+ console.log(response.text()); // hello MCP
41
50
  await registry.close();
42
51
  ```
43
52
 
@@ -184,12 +193,20 @@ const pinned = await registry.connect('strict-server', {
184
193
 
185
194
  After connecting, `client.getProtocolEra()` returns `'modern'` or `'legacy'` and `client.getNegotiatedProtocolVersion()` the revision the server settled on.
186
195
 
187
- Note: with `mode: 'auto'` against a stdio server, a legacy server that never answers the `server/discover` probe costs the full request timeout (60s) before the client falls back to the 2025 sequence. The probe ends fast when the server answers it at all — with any reply, even a "method not found" error.
196
+ With `mode: 'auto'` against a stdio server, a legacy server that never answers the `server/discover` probe costs the full 60-second request timeout before the client falls back to the 2025 sequence. Any reply, including a "method not found" error, ends the probe.
188
197
 
189
198
  ## Requirements
190
199
 
191
- - Node.js >= 22
200
+ - Node.js >= 20
201
+
202
+ ## Agent skill
203
+
204
+ If a coding agent will use this package, install its agent guidance globally:
205
+
206
+ ```bash
207
+ npx skills add https://github.com/mcp-z/client.git -g -s mcp-z-client
208
+ ```
192
209
 
193
- ### Documentation
210
+ ## Documentation
194
211
 
195
212
  [API Docs](https://mcp-z.github.io/client)
@@ -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>".concat(errorMessage, "</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n "));
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>${errorMessage}</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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"}
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 SIGKILL after timeout.
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 SIGKILL after timeout.
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 resolveOnce = function resolveOnce(timedOut) {
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: 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
- try {
350
- // Check again before SIGKILL
351
- if (child.exitCode === null && !child.killed) {
352
- child.kill('SIGKILL');
353
- wasKilled = true;
354
- }
355
- } catch (_) {}
356
- // Even if kill fails, resolve
357
- resolveOnce(true);
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
- // Send graceful shutdown signal
369
- try {
370
- // Check one more time before killing
371
- if (child.exitCode !== null) {
372
- resolveOnce(false);
373
- return;
374
- }
375
- var killed = child.kill(signal);
376
- // If kill returned false, process already exited
377
- if (!killed) {
378
- resolveOnce(false);
379
- }
380
- } catch (_err) {
381
- // If kill throws, process is gone or unreachable
382
- resolveOnce(false);
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":"AAAA;;;CAGC;;;;+BAkHeA;;;eAAAA;;;6BAhH+D;+DACtD;wBACF;2BACU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DjC;;CAEC,GACD,SAASC,aAAaC,GAA4B;IAChD,IAAMC,SAAS,mBAAKC,SAAQF,GAAG,EAAMA,OAAO,CAAC;IAC7C,IAAMG,SAAiC,CAAC;QACnC,kCAAA,2BAAA;;QAAL,QAAK,YAAsBC,OAAOC,OAAO,CAACJ,4BAArC,SAAA,6BAAA,QAAA,yBAAA,iCAA8C;YAA9C,mCAAA,iBAAOK,sBAAKC;YACf,IAAIA,UAAUC,WAAW;gBACvBL,MAAM,CAACG,IAAI,GAAGC;YAChB;QACF;;QAJK;QAAA;;;iBAAA,6BAAA;gBAAA;;;gBAAA;sBAAA;;;;IAKL,OAAOJ;AACT;AAEA,SAASM,4BAA4BC,GAAW,EAAEC,KAAc;IAC9D,IAAMC,OAAO;QAAC;QAAQC,OAAOH;QAAM;KAAK;IACxC,IAAIC,OAAOC,KAAKE,IAAI,CAAC;IAErB,OAAO,IAAIC,QAAQ,SAACC,SAASC;QAC3B,IAAMC,WAAWC,IAAAA,oBAAK,EAAC,gBAAgBP,MAAM;YAAEQ,OAAO;YAAUC,aAAa;QAAK;QAClFH,SAASI,IAAI,CAAC,SAASL;QACvBC,SAASI,IAAI,CAAC,SAAS,SAACC;YACtB,IAAIA,SAAS,GAAG;gBACdP;gBACA;YACF;YACAC,OAAO,IAAIO,MAAM,AA7FvB,AA6FwB,iEAAyDd,KAAI,gBAAgC,OAAlBa,iBAAAA,kBAAAA,OAAQ,WAAU;QACjH;IACF;AACF;AAqBO,SAASzB,aAAa2B,IAAyB;QAGxCA,WACEA,aACAA;IAJd,IAAMC,OAAOD,KAAKC,IAAI;IACtB,IAAMC,UAAUF,KAAKE,OAAO;IAC5B,IAAMC,OAAMH,YAAAA,KAAKG,GAAG,cAARH,uBAAAA,YAAYvB,SAAQ0B,GAAG;IACnC,IAAMR,SAAQK,cAAAA,KAAKL,KAAK,cAAVK,yBAAAA,cAAc;IAC5B,IAAMI,SAAQJ,cAAAA,KAAKI,KAAK,cAAVJ,yBAAAA,cAAcvB,SAAQ4B,QAAQ,KAAK;IAEjD,0DAA0D;IAC1D,IAAMlB,OAAOa,KAAKb,IAAI,GAAGmB,IAAAA,6BAAgB,EAACN,KAAKb,IAAI,EAAEgB,OAAO,EAAE;IAE9D,8BAA8B;IAC9B,IAAM5B,MAAMD,aAAa0B,KAAKzB,GAAG;IAEjC,0CAA0C;IAC1C,IAAMgC,iBAAiB;QACrBN,MAAAA;QACAC,SAAAA;QACAf,MAAAA;QACAgB,KAAAA;QACA5B,KAAAA;QACAoB,OAAAA;QACAS,OAAAA;IACF;IAEA,sBAAsB;IACtBI,gBAAM,CAACC,IAAI,CAAC,AAAC,IAAcP,OAAXD,MAAK,QAAiBd,OAAXe,SAAQ,KAAkB,OAAff,KAAKuB,IAAI,CAAC;IAEhD,oBAAoB;IACpB,IAAMC,YAA0B;QAAER,KAAAA;QAAK5B,KAAAA;QAAKoB,OAAAA;QAAOS,OAAAA;IAAM;IACzD,IAAMQ,QAAQlB,IAAAA,oBAAK,EAACQ,SAASf,MAAMwB;IAEnC,8BAA8B;IAC9B,IAAIC,MAAMC,MAAM,EACdD,MAAMC,MAAM,CAACC,EAAE,CAAC,QAAQ,SAACC;QACvBtC,SAAQoC,MAAM,CAACG,KAAK,CAACD;IACvB;IAEF,2BAA2B;IAC3BH,MAAME,EAAE,CAAC,QAAQ,SAAChB,MAAMmB;eAAQT,gBAAM,CAACC,IAAI,CAAC,AAAC,IAAyBX,OAAtBG,MAAK,mBAAiCgB,OAAhBnB,MAAK,aAAyB,OAAdmB,OAAO,QAAO;;IACpGL,MAAME,EAAE,CAAC,SAAS,SAACI;eAAQV,gBAAM,CAACC,IAAI,CAAC,AAAC,IAA2BS,OAAxBjB,MAAK,qBAA+B,OAAZiB,IAAIC,OAAO;;IAE9E,8CAA8C;IAC9C,IAAMC,OAAO;YAAOC,0EAAyB,UAAUrB,0EAA+B,CAAC;;gBAMnEA,iBAAZsB,WAIAC;;gBATN,wCAAwC;gBACxC,IAAIX,MAAMY,QAAQ,KAAK,QAAQZ,MAAMa,UAAU,KAAK,MAAM;oBACxD;;wBAAO;4BAAEC,UAAU;4BAAOC,QAAQ;wBAAM;;gBAC1C;gBAEML,aAAYtB,kBAAAA,OAAKsB,SAAS,cAAdtB,6BAAAA,kBAAkB;gBAEpC,+DAA+D;gBAC/D,0EAA0E;gBACpEuB,eAAe,IAAIjC,QAAgD,SAACC,SAASC;oBACjF,IAAIoC,aAAa;oBACjB,IAAIC,YAAY;oBAChB,IAAIH,WAAW;oBAEf,IAAMI,cAAc,qBAACC;wBACnB,IAAIH,YAAY;wBAChBA,aAAa;wBACbI,aAAaC;wBACb1C,QAAQ;4BAAEmC,UAAUK;4BAAYJ,QAAQE;wBAAU;oBACpD;oBAEA,IAAMK,aAAa,oBAACC;wBAClB,IAAIP,YAAY;wBAChBA,aAAa;wBACbI,aAAaC;wBACbzC,OAAO2C;oBACT;oBAEA,gCAAgC;oBAChC,IAAMF,UAAUG,WAAW;wBACzBV,WAAW;wBACX,IAAMW,YAAY;;oCASPF;;;;;;;;;;iDAPH1D,CAAAA,SAAQ4B,QAAQ,KAAK,WAAWO,MAAM3B,GAAG,AAAD,GAAxCR;;;;4CACF;;gDAAMO,4BAA4B4B,MAAM3B,GAAG,EAAE;;;4CAA7C;4CACA4C,YAAY;;;;;;4CACP,IAAIjB,MAAMY,QAAQ,KAAK,QAAQ,CAACZ,MAAMe,MAAM,EAAE;gDACnDE,YAAYjB,MAAM0B,IAAI,CAAC;4CACzB;;;4CACAR,YAAY;;;;;;4CACLK;4CACPD,WAAWC,AAAK,YAALA,OAAiBpC,SAAQoC,QAAQ,IAAIpC,MAAMX,OAAO+C;;;;;;;;;;;4BAEjE;;wBACA,KAAKE;oBACP,GAAGf;oBAEH,gEAAgE;oBAChEV,MAAMf,IAAI,CAAC,SAAS;wBAClB,IAAI,CAAC6B,UAAUI,YAAY;oBAC7B;oBAEA,qDAAqD;oBACrD,sEAAsE;oBACtElB,MAAMf,IAAI,CAAC,SAAS;wBAClBiC,YAAY;oBACd;oBAEA,IAAMS,kBAAkB;;gCAYbJ;;;;;;;;;;wCAVP,IAAIvB,MAAMY,QAAQ,KAAK,QAAQZ,MAAMa,UAAU,KAAK,MAAM;4CACxDK,YAAY;4CACZ;;;wCACF;6CAEIrD,CAAAA,SAAQ4B,QAAQ,KAAK,WAAWO,MAAM3B,GAAG,AAAD,GAAxCR;;;;wCACF;;4CAAMO,4BAA4B4B,MAAM3B,GAAG,EAAE;;;wCAA7C;;;;;;wCACK,IAAI,CAAC2B,MAAM0B,IAAI,CAACjB,SAAS;4CAC9BS,YAAY;wCACd;;;;;;;;wCACOK;wCACPD,WAAWC,AAAK,YAALA,OAAiBpC,SAAQoC,QAAQ,IAAIpC,MAAMX,OAAO+C;;;;;;;;;;;wBAEjE;;oBACA,KAAKI;gBACP;gBAEA;;oBAAOhB;;;QACT;;IAEA,OAAO;QACLiB,QAAQjC;QACR9B,SAASmC;QACT6B,OAAOrB;IACT;AACF"}
@@ -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 === '~' || filePath.startsWith('~/')) {
103
- filePath = filePath.replace(/^~/, _os.homedir());
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 ./ or ../ or / )
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 === '~' || filePath.startsWith('~/')) {\n filePath = filePath.replace(/^~/, os.homedir());\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 ./ or ../ or / )\n if (value && 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","startsWith","replace","os","homedir","path","isAbsolute","resolve","args","map","arg","flagMatch","match","flag","value","includes","test"],"mappings":"AAAA;;;CAGC;;;;;;;;;;;QA2DeA;eAAAA;;QArCAC;eAAAA;;;0DApBI;4DACE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBf,SAASA,YAAYC,QAAgB,EAAEC,GAAW;IACvD,6BAA6B;IAC7B,IAAID,aAAa,OAAOA,SAASE,UAAU,CAAC,OAAO;QACjDF,WAAWA,SAASG,OAAO,CAAC,MAAMC,IAAGC,OAAO;IAC9C;IAEA,4BAA4B;IAC5B,IAAIC,MAAKC,UAAU,CAACP,WAAW;QAC7B,OAAOA;IACT;IAEA,oCAAoC;IACpC,OAAOM,MAAKE,OAAO,CAACP,KAAKD;AAC3B;AAwBO,SAASF,iBAAiBW,IAAc,EAAER,GAAW;IAC1D,OAAOQ,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,yEAAyE;YACzE,IAAIG,SAASA,MAAMC,QAAQ,CAAC,MAAM;gBAChC,2BAA2B;gBAC3B,IAAI,gCAAgCC,IAAI,CAACF,QAAQ;oBAC/C,OAAOJ;gBACT;gBACA,OAAO,AAAC,GAAUZ,OAARe,MAAK,KAA2B,OAAxBf,YAAYgB,OAAOd;YACvC;YACA,OAAOU,KAAK,yDAAyD;QACvE;QAEA,qDAAqD;QACrD,IAAIA,IAAIT,UAAU,CAAC,MAAM;YACvB,OAAOS,KAAK,4CAA4C;QAC1D;QAEA,sCAAsC;QACtC,IAAI,gCAAgCM,IAAI,CAACN,MAAM;YAC7C,OAAOA,KAAK,oBAAoB;QAClC;QAEA,4EAA4E;QAC5E,IAAIA,IAAIT,UAAU,CAAC,QAAS,CAACS,IAAIK,QAAQ,CAAC,QAAQ,CAACL,IAAIK,QAAQ,CAAC,OAAQ;YACtE,OAAOL,KAAK,iCAAiC;QAC/C;QAEA,0CAA0C;QAC1C,OAAOZ,YAAYY,KAAKV;IAC1B;AACF"}
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>${errorMessage}</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>${errorMessage}</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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"}
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 SIGKILL after timeout.
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
- const resolveOnce = (timedOut)=>{
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
- try {
107
- // Check again before SIGKILL
108
- if (child.exitCode === null && !child.killed) {
109
- child.kill('SIGKILL');
110
- wasKilled = true;
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
- } catch (_) {}
113
- // Even if kill fails, resolve
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
- // Send graceful shutdown signal
126
- try {
127
- // Check one more time before killing
128
- if (child.exitCode !== null) {
129
- resolveOnce(false);
130
- return;
131
- }
132
- const killed = child.kill(signal);
133
- // If kill returned false, process already exited
134
- if (!killed) {
135
- resolveOnce(false);
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
- } catch (_err) {
138
- // If kill throws, process is gone or unreachable
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":"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"}
@@ -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 === '~' || filePath.startsWith('~/')) {
25
- filePath = filePath.replace(/^~/, os.homedir());
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 ./ or ../ or / )
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 === '~' || filePath.startsWith('~/')) {\n filePath = filePath.replace(/^~/, os.homedir());\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 ./ or ../ or / )\n if (value && 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","startsWith","replace","homedir","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,OAAOA,SAASE,UAAU,CAAC,OAAO;QACjDF,WAAWA,SAASG,OAAO,CAAC,MAAMN,GAAGO,OAAO;IAC9C;IAEA,4BAA4B;IAC5B,IAAIN,KAAKO,UAAU,CAACL,WAAW;QAC7B,OAAOA;IACT;IAEA,oCAAoC;IACpC,OAAOF,KAAKQ,OAAO,CAACL,KAAKD;AAC3B;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASO,iBAAiBC,IAAc,EAAEP,GAAW;IAC1D,OAAOO,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,yEAAyE;YACzE,IAAIG,SAASA,MAAMC,QAAQ,CAAC,MAAM;gBAChC,2BAA2B;gBAC3B,IAAI,gCAAgCC,IAAI,CAACF,QAAQ;oBAC/C,OAAOJ;gBACT;gBACA,OAAO,GAAGG,KAAK,CAAC,EAAEd,YAAYe,OAAOb,MAAM;YAC7C;YACA,OAAOS,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,OAAOX,YAAYW,KAAKT;IAC1B;AACF"}
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.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
  },
@@ -69,6 +71,7 @@
69
71
  },
70
72
  "devDependencies": {
71
73
  "@modelcontextprotocol/sdk": "^1.30.0",
74
+ "@modelcontextprotocol/server": "^2.0.0",
72
75
  "@types/express": "^5.0.6",
73
76
  "@types/mocha": "^10.0.10",
74
77
  "@types/node": "^26.2.0",