@apify/mcpc 0.2.6 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/CONTRIBUTING.md +12 -0
  3. package/NOTICE +27 -0
  4. package/README.md +177 -223
  5. package/_config.yml +30 -0
  6. package/client-logo.svg +79 -0
  7. package/client-metadata.json +16 -0
  8. package/dist/bridge/index.js +25 -0
  9. package/dist/bridge/index.js.map +1 -1
  10. package/dist/cli/commands/auth.d.ts +2 -1
  11. package/dist/cli/commands/auth.d.ts.map +1 -1
  12. package/dist/cli/commands/auth.js +27 -12
  13. package/dist/cli/commands/auth.js.map +1 -1
  14. package/dist/cli/commands/grep.js +4 -4
  15. package/dist/cli/commands/grep.js.map +1 -1
  16. package/dist/cli/commands/sessions.d.ts +21 -2
  17. package/dist/cli/commands/sessions.d.ts.map +1 -1
  18. package/dist/cli/commands/sessions.js +366 -66
  19. package/dist/cli/commands/sessions.js.map +1 -1
  20. package/dist/cli/commands/tasks.d.ts.map +1 -1
  21. package/dist/cli/commands/tasks.js +6 -3
  22. package/dist/cli/commands/tasks.js.map +1 -1
  23. package/dist/cli/commands/tools.d.ts.map +1 -1
  24. package/dist/cli/commands/tools.js +42 -18
  25. package/dist/cli/commands/tools.js.map +1 -1
  26. package/dist/cli/commands/x402.d.ts.map +1 -1
  27. package/dist/cli/commands/x402.js +6 -6
  28. package/dist/cli/commands/x402.js.map +1 -1
  29. package/dist/cli/helpers.d.ts.map +1 -1
  30. package/dist/cli/helpers.js +3 -6
  31. package/dist/cli/helpers.js.map +1 -1
  32. package/dist/cli/index.js +110 -73
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/output.d.ts +13 -5
  35. package/dist/cli/output.d.ts.map +1 -1
  36. package/dist/cli/output.js +176 -77
  37. package/dist/cli/output.js.map +1 -1
  38. package/dist/cli/parser.d.ts.map +1 -1
  39. package/dist/cli/parser.js +19 -9
  40. package/dist/cli/parser.js.map +1 -1
  41. package/dist/cli/shell.js +19 -19
  42. package/dist/cli/shell.js.map +1 -1
  43. package/dist/cli/tool-result.d.ts +1 -1
  44. package/dist/cli/tool-result.d.ts.map +1 -1
  45. package/dist/cli/tool-result.js +20 -15
  46. package/dist/cli/tool-result.js.map +1 -1
  47. package/dist/core/factory.d.ts +1 -0
  48. package/dist/core/factory.d.ts.map +1 -1
  49. package/dist/core/factory.js +3 -0
  50. package/dist/core/factory.js.map +1 -1
  51. package/dist/core/transports.d.ts +5 -1
  52. package/dist/core/transports.d.ts.map +1 -1
  53. package/dist/core/transports.js +26 -4
  54. package/dist/core/transports.js.map +1 -1
  55. package/dist/lib/auth/auth-page.d.ts +13 -0
  56. package/dist/lib/auth/auth-page.d.ts.map +1 -0
  57. package/dist/lib/auth/auth-page.js +129 -0
  58. package/dist/lib/auth/auth-page.js.map +1 -0
  59. package/dist/lib/auth/oauth-flow.d.ts +1 -1
  60. package/dist/lib/auth/oauth-flow.d.ts.map +1 -1
  61. package/dist/lib/auth/oauth-flow.js +50 -67
  62. package/dist/lib/auth/oauth-flow.js.map +1 -1
  63. package/dist/lib/auth/oauth-provider.d.ts.map +1 -1
  64. package/dist/lib/auth/oauth-provider.js +2 -0
  65. package/dist/lib/auth/oauth-provider.js.map +1 -1
  66. package/dist/lib/auth/oauth-utils.d.ts +3 -0
  67. package/dist/lib/auth/oauth-utils.d.ts.map +1 -1
  68. package/dist/lib/auth/oauth-utils.js +32 -1
  69. package/dist/lib/auth/oauth-utils.js.map +1 -1
  70. package/dist/lib/auth/profiles.d.ts.map +1 -1
  71. package/dist/lib/auth/profiles.js +3 -3
  72. package/dist/lib/auth/profiles.js.map +1 -1
  73. package/dist/lib/bridge-manager.d.ts.map +1 -1
  74. package/dist/lib/bridge-manager.js +21 -9
  75. package/dist/lib/bridge-manager.js.map +1 -1
  76. package/dist/lib/config.d.ts +21 -0
  77. package/dist/lib/config.d.ts.map +1 -1
  78. package/dist/lib/config.js +94 -4
  79. package/dist/lib/config.js.map +1 -1
  80. package/dist/lib/errors.d.ts +1 -0
  81. package/dist/lib/errors.d.ts.map +1 -1
  82. package/dist/lib/errors.js +3 -0
  83. package/dist/lib/errors.js.map +1 -1
  84. package/dist/lib/sessions.d.ts.map +1 -1
  85. package/dist/lib/sessions.js +5 -4
  86. package/dist/lib/sessions.js.map +1 -1
  87. package/dist/lib/utils.d.ts +1 -0
  88. package/dist/lib/utils.d.ts.map +1 -1
  89. package/dist/lib/utils.js +21 -1
  90. package/dist/lib/utils.js.map +1 -1
  91. package/dist/lib/wallets.js +3 -3
  92. package/dist/lib/wallets.js.map +1 -1
  93. package/package.json +2 -1
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'net';
2
- import { isValidSessionName, generateSessionName, normalizeServerUrl, validateProfileName, isProcessAlive, getServerHost, redactHeaders, } from '../../lib/index.js';
2
+ import { isValidSessionName, generateSessionName, normalizeServerUrl, validateProfileName, isProcessAlive, getServerHost, getLogsDir, redactHeaders, } from '../../lib/index.js';
3
3
  import { DISCONNECTED_THRESHOLD_MS } from '../../lib/types.js';
4
- import { formatOutput, formatSuccess, formatWarning, formatError, formatSessionLine, formatServerDetails, } from '../output.js';
4
+ import { formatOutput, formatSuccess, formatWarning, formatError, formatSessionLine, formatServerDetails, theme, } from '../output.js';
5
5
  import { withMcpClient, resolveTarget, resolveAuthProfile } from '../helpers.js';
6
6
  import { listAuthProfiles } from '../../lib/auth/profiles.js';
7
7
  import { sessionExists, deleteSession, saveSession, updateSession, consolidateSessions, getSession, loadSessions, } from '../../lib/sessions.js';
@@ -12,7 +12,7 @@ import { getWallet } from '../../lib/wallets.js';
12
12
  import chalk from 'chalk';
13
13
  import { createLogger } from '../../lib/logger.js';
14
14
  import { parseProxyArg } from '../parser.js';
15
- import { loadConfig, listServers } from '../../lib/config.js';
15
+ import { loadConfig, listServers, isStdioEntry, discoverMcpConfigFiles, getStandardMcpConfigPaths, } from '../../lib/config.js';
16
16
  const logger = createLogger('sessions');
17
17
  async function checkPortAvailable(host, port) {
18
18
  return new Promise((resolve) => {
@@ -81,7 +81,7 @@ export async function resolveSessionName(parsed, options) {
81
81
  const storage = await loadSessions();
82
82
  if (!(candidateName in storage.sessions)) {
83
83
  if (options.outputMode === 'human') {
84
- console.log(chalk.cyan(`Using session name: ${candidateName}`));
84
+ console.log(theme.cyan(`Using session name: ${candidateName}`));
85
85
  }
86
86
  return candidateName;
87
87
  }
@@ -89,7 +89,7 @@ export async function resolveSessionName(parsed, options) {
89
89
  const suffixed = `${candidateName}-${i}`;
90
90
  if (isValidSessionName(suffixed) && !(suffixed in storage.sessions)) {
91
91
  if (options.outputMode === 'human') {
92
- console.log(chalk.cyan(`Using session name: ${suffixed}`));
92
+ console.log(theme.cyan(`Using session name: ${suffixed}`));
93
93
  }
94
94
  return suffixed;
95
95
  }
@@ -97,6 +97,40 @@ export async function resolveSessionName(parsed, options) {
97
97
  throw new ClientError(`Cannot auto-generate session name: too many sessions for this server.\n` +
98
98
  `Specify a name explicitly: mcpc connect ${parsed.type === 'url' ? parsed.url : `${parsed.file}:${parsed.entry}`} @my-session`);
99
99
  }
100
+ async function buildConnectResultEntry(sessionName, status, options) {
101
+ return await withMcpClient(sessionName, {
102
+ outputMode: 'json',
103
+ hideTarget: true,
104
+ ...(options.verbose && { verbose: options.verbose }),
105
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
106
+ }, async (client, context) => {
107
+ const serverDetails = await client.getServerDetails();
108
+ const tools = (await client.listAllTools()).tools;
109
+ const server = context.serverConfig
110
+ ? {
111
+ ...context.serverConfig,
112
+ ...(context.serverConfig.headers && {
113
+ headers: redactHeaders(context.serverConfig.headers),
114
+ }),
115
+ }
116
+ : undefined;
117
+ return {
118
+ _mcpc: {
119
+ sessionName: context.sessionName ?? sessionName,
120
+ ...(context.profileName && { profileName: context.profileName }),
121
+ ...(server && { server }),
122
+ ...(options.configFile && { configFile: options.configFile }),
123
+ ...(options.entry && { entry: options.entry }),
124
+ status,
125
+ },
126
+ ...(serverDetails.protocolVersion && { protocolVersion: serverDetails.protocolVersion }),
127
+ ...(serverDetails.capabilities && { capabilities: serverDetails.capabilities }),
128
+ ...(serverDetails.serverInfo && { serverInfo: serverDetails.serverInfo }),
129
+ ...(serverDetails.instructions && { instructions: serverDetails.instructions }),
130
+ ...(tools.length > 0 && { toolNames: tools.map((t) => t.name) }),
131
+ };
132
+ });
133
+ }
100
134
  export async function connectSession(target, name, options) {
101
135
  if (!isValidSessionName(name)) {
102
136
  throw new ClientError(`Invalid session name: ${name}\n` +
@@ -126,12 +160,21 @@ export async function connectSession(target, name, options) {
126
160
  console.log(formatSuccess(`Session ${name} is already active`));
127
161
  }
128
162
  if (!options.skipDetails) {
129
- await showServerDetails(name, { ...options, hideTarget: false });
163
+ if (options.outputMode === 'json') {
164
+ const entry = await buildConnectResultEntry(name, 'active', {
165
+ ...(options.verbose && { verbose: options.verbose }),
166
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
167
+ });
168
+ console.log(formatOutput([entry], 'json'));
169
+ }
170
+ else {
171
+ await showServerDetails(name, { ...options, hideTarget: false });
172
+ }
130
173
  }
131
174
  return;
132
175
  }
133
176
  if (options.outputMode === 'human' && !options.quiet) {
134
- console.log(chalk.yellow(`Session ${name} exists but bridge is ${bridgeStatus}, reconnecting...`));
177
+ console.log(theme.yellow(`Session ${name} exists but bridge is ${bridgeStatus}, reconnecting...`));
135
178
  }
136
179
  try {
137
180
  await stopBridge(name);
@@ -258,11 +301,18 @@ export async function connectSession(target, name, options) {
258
301
  return;
259
302
  }
260
303
  try {
261
- await showServerDetails(name, {
262
- ...options,
263
- hideTarget: false,
264
- });
265
- if (options.outputMode === 'human') {
304
+ if (options.outputMode === 'json') {
305
+ const entry = await buildConnectResultEntry(name, 'created', {
306
+ ...(options.verbose && { verbose: options.verbose }),
307
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
308
+ });
309
+ console.log(formatOutput([entry], 'json'));
310
+ }
311
+ else {
312
+ await showServerDetails(name, {
313
+ ...options,
314
+ hideTarget: false,
315
+ });
266
316
  console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
267
317
  }
268
318
  }
@@ -271,15 +321,26 @@ export async function connectSession(target, name, options) {
271
321
  throw detailsError;
272
322
  }
273
323
  if (detailsError instanceof Error && isAuthenticationError(detailsError.message)) {
274
- throw createServerAuthError(serverConfig.url || target, { sessionName: name });
324
+ const logPath = `${getLogsDir()}/bridge-${name}.log`;
325
+ throw createServerAuthError(serverConfig.url || target, { sessionName: name, logPath });
275
326
  }
276
- if (options.outputMode === 'human') {
277
- const errorMsg = detailsError instanceof Error ? detailsError.message : String(detailsError);
327
+ const errorMsg = detailsError instanceof Error ? detailsError.message : String(detailsError);
328
+ if (options.outputMode === 'json') {
329
+ const failed = {
330
+ _mcpc: {
331
+ sessionName: name,
332
+ status: 'failed',
333
+ error: errorMsg,
334
+ },
335
+ };
336
+ console.log(formatOutput([failed], 'json'));
337
+ }
338
+ else {
278
339
  console.log(formatWarning(`Session ${name} created but server is not responding: ${errorMsg}\n` +
279
340
  ` The session will auto-recover when the server becomes available.\n` +
280
341
  ` Check status with: mcpc ${name}`));
281
342
  }
282
- logger.debug(`showServerDetails failed for new session ${name}: ${detailsError.message}`);
343
+ logger.debug(`showServerDetails failed for new session ${name}: ${errorMsg}`);
283
344
  }
284
345
  }
285
346
  export function getBridgeStatus(session) {
@@ -306,19 +367,19 @@ export function getBridgeStatus(session) {
306
367
  export function formatBridgeStatus(status) {
307
368
  switch (status) {
308
369
  case 'live':
309
- return { dot: chalk.green('●'), text: chalk.green('live') };
370
+ return { dot: theme.green('●'), text: theme.green('live') };
310
371
  case 'connecting':
311
- return { dot: chalk.yellow('●'), text: chalk.yellow('connecting') };
372
+ return { dot: theme.yellow('●'), text: theme.yellow('connecting') };
312
373
  case 'reconnecting':
313
- return { dot: chalk.yellow('●'), text: chalk.yellow('reconnecting') };
374
+ return { dot: theme.yellow('●'), text: theme.yellow('reconnecting') };
314
375
  case 'disconnected':
315
- return { dot: chalk.yellow('●'), text: chalk.yellow('disconnected') };
376
+ return { dot: theme.yellow('●'), text: theme.yellow('disconnected') };
316
377
  case 'crashed':
317
- return { dot: chalk.yellow('○'), text: chalk.yellow('crashed') };
378
+ return { dot: theme.yellow('○'), text: theme.yellow('crashed') };
318
379
  case 'unauthorized':
319
- return { dot: chalk.red('○'), text: chalk.red('unauthorized') };
380
+ return { dot: theme.red('○'), text: theme.red('unauthorized') };
320
381
  case 'expired':
321
- return { dot: chalk.red('○'), text: chalk.red('expired') };
382
+ return { dot: theme.red('○'), text: theme.red('expired') };
322
383
  }
323
384
  }
324
385
  export function formatTimeAgo(isoDate) {
@@ -402,7 +463,7 @@ export async function listSessionsAndAuthProfiles(options) {
402
463
  console.log(chalk.bold('Saved OAuth profiles:'));
403
464
  for (const profile of profiles) {
404
465
  const hostStr = getServerHost(profile.serverUrl);
405
- const nameStr = chalk.magenta(profile.name);
466
+ const nameStr = theme.magenta(profile.name);
406
467
  const userStr = profile.userEmail || profile.userName || '';
407
468
  const timeAgo = formatTimeAgo(profile.refreshedAt || profile.createdAt);
408
469
  const timeLabel = profile.refreshedAt ? 'refreshed' : 'created';
@@ -487,7 +548,7 @@ export async function restartSession(name, options) {
487
548
  throw new ClientError(`Session not found: ${name}`);
488
549
  }
489
550
  if (options.outputMode === 'human') {
490
- console.log(chalk.yellow(`Restarting session ${name}...`));
551
+ console.log(theme.yellow(`Restarting session ${name}...`));
491
552
  }
492
553
  try {
493
554
  await stopBridge(name);
@@ -536,6 +597,7 @@ export async function restartSession(name, options) {
536
597
  logger.debug(`Session ${name} restarted with bridge PID: ${pid}`);
537
598
  if (options.outputMode === 'human') {
538
599
  console.log(formatSuccess(`Session ${name} restarted`));
600
+ console.log(chalk.dim('Note: previous session state was lost (e.g. added tools, resource subscriptions, async tasks)'));
539
601
  }
540
602
  await showServerDetails(name, {
541
603
  ...options,
@@ -556,19 +618,7 @@ export async function restartSession(name, options) {
556
618
  throw error;
557
619
  }
558
620
  }
559
- export async function connectAllFromConfig(configFile, options) {
560
- const config = loadConfig(configFile);
561
- const serverNames = listServers(config);
562
- if (serverNames.length === 0) {
563
- throw new ClientError(`No servers found in config file: ${configFile}`);
564
- }
565
- if (options.outputMode === 'human') {
566
- console.log(chalk.cyan(`Connecting ${serverNames.length} server${serverNames.length === 1 ? '' : 's'} from ${configFile}...`));
567
- }
568
- const entries = serverNames.map((entry) => ({
569
- entry,
570
- sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
571
- }));
621
+ async function bulkConnectEntries(entries, options) {
572
622
  const liveSet = new Set();
573
623
  for (const { sessionName } of entries) {
574
624
  const session = await getSession(sessionName);
@@ -576,57 +626,43 @@ export async function connectAllFromConfig(configFile, options) {
576
626
  liveSet.add(sessionName);
577
627
  }
578
628
  }
579
- const settled = await Promise.allSettled(entries.map(async ({ entry, sessionName }) => connectSession(entry, sessionName, {
629
+ const settled = await Promise.allSettled(entries.map(async ({ entry, sessionName, configFile }) => connectSession(entry, sessionName, {
580
630
  ...options,
581
631
  config: configFile,
582
632
  skipDetails: true,
583
633
  quiet: true,
584
634
  })));
585
635
  const results = settled.map((outcome, i) => {
586
- const { entry, sessionName } = entries[i];
636
+ const base = entries[i];
587
637
  if (outcome.status === 'fulfilled') {
588
- if (liveSet.has(sessionName)) {
589
- return { entry, sessionName, status: 'active' };
590
- }
591
- return { entry, sessionName, status: 'created' };
638
+ return { ...base, status: liveSet.has(base.sessionName) ? 'active' : 'created' };
592
639
  }
593
640
  const error = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
594
- return { entry, sessionName, status: 'failed', error };
641
+ return { ...base, status: 'failed', error };
595
642
  });
596
643
  if (options.outputMode === 'human') {
597
644
  for (const r of results) {
598
- const name = chalk.cyan(r.sessionName);
645
+ const name = theme.cyan(r.sessionName);
599
646
  switch (r.status) {
600
647
  case 'created':
601
- console.log(` ${chalk.yellow('●')} ${name} ${chalk.yellow('connecting')}`);
648
+ console.log(` ${theme.yellow('●')} ${name} ${theme.yellow('connecting')}`);
602
649
  break;
603
650
  case 'active':
604
- console.log(` ${chalk.green('●')} ${name} ${chalk.dim('already active')}`);
605
- break;
606
- case 'reconnected':
607
- console.log(` ${chalk.yellow('●')} ${name} ${chalk.yellow('reconnecting')}`);
651
+ console.log(` ${theme.green('●')} ${name} ${chalk.dim('already active')}`);
608
652
  break;
609
653
  case 'failed':
610
- console.log(` ${chalk.red('●')} ${name} ${chalk.red('failed')}${r.error ? chalk.dim(` — ${r.error}`) : ''}`);
654
+ console.log(` ${theme.red('●')} ${name} ${theme.red('failed')}${r.error ? chalk.dim(` — ${r.error}`) : ''}`);
611
655
  break;
612
656
  }
613
657
  }
614
658
  }
659
+ return results;
660
+ }
661
+ function printBulkConnectSummary(results, options) {
615
662
  const active = results.filter((r) => r.status === 'active').length;
616
- const connecting = results.filter((r) => r.status === 'created' || r.status === 'reconnected').length;
663
+ const connecting = results.filter((r) => r.status === 'created').length;
617
664
  const failed = results.filter((r) => r.status === 'failed').length;
618
- if (options.outputMode === 'json') {
619
- console.log(formatOutput({
620
- configFile,
621
- results: results.map((r) => ({
622
- entry: r.entry,
623
- sessionName: r.sessionName,
624
- status: r.status,
625
- ...(r.error && { error: r.error }),
626
- })),
627
- }, 'json'));
628
- }
629
- else if (results.length > 1) {
665
+ if (options.outputMode === 'human' && results.length > 1) {
630
666
  const parts = [];
631
667
  if (active > 0)
632
668
  parts.push(`${active} already active`);
@@ -642,10 +678,274 @@ export async function connectAllFromConfig(configFile, options) {
642
678
  console.log(formatWarning(summary));
643
679
  }
644
680
  }
645
- if (active + connecting === 0) {
681
+ return { active, connecting, failed };
682
+ }
683
+ export async function connectAllFromConfig(configFile, options) {
684
+ const config = loadConfig(configFile);
685
+ const allNames = listServers(config);
686
+ if (allNames.length === 0) {
687
+ throw new ClientError(`No servers found in config file: ${configFile}`);
688
+ }
689
+ const stdioSkipped = [];
690
+ const serverNames = allNames.filter((name) => {
691
+ if (!options.stdio && isStdioEntry(config, name)) {
692
+ stdioSkipped.push(name);
693
+ return false;
694
+ }
695
+ return true;
696
+ });
697
+ if (serverNames.length === 0) {
698
+ if (options.outputMode === 'json') {
699
+ const skippedEntries = stdioSkipped.map((entry) => ({
700
+ _mcpc: {
701
+ sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
702
+ configFile,
703
+ entry,
704
+ status: 'skipped',
705
+ skipReason: 'stdio',
706
+ },
707
+ }));
708
+ console.log(formatOutput(skippedEntries, 'json'));
709
+ return;
710
+ }
711
+ throw new ClientError(`All ${allNames.length} server${allNames.length === 1 ? '' : 's'} in ${configFile} use stdio transport.\n` +
712
+ `Pass --stdio to include them: mcpc connect ${configFile} --stdio`);
713
+ }
714
+ if (options.outputMode === 'human') {
715
+ console.log(theme.cyan(`Connecting ${serverNames.length} server${serverNames.length === 1 ? '' : 's'} from ${configFile}...`));
716
+ if (stdioSkipped.length > 0) {
717
+ console.log(chalk.dim(` skipping ${stdioSkipped.length} stdio server${stdioSkipped.length === 1 ? '' : 's'} ` +
718
+ `(${stdioSkipped.join(', ')}), pass --stdio to include`));
719
+ }
720
+ }
721
+ const entries = serverNames.map((entry) => ({
722
+ configFile,
723
+ entry,
724
+ sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
725
+ }));
726
+ const results = await bulkConnectEntries(entries, options);
727
+ if (options.outputMode === 'json') {
728
+ const resultEntries = await buildBulkConnectEntries(results, options);
729
+ const skippedEntries = stdioSkipped.map((entry) => ({
730
+ _mcpc: {
731
+ sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
732
+ configFile,
733
+ entry,
734
+ status: 'skipped',
735
+ skipReason: 'stdio',
736
+ },
737
+ }));
738
+ console.log(formatOutput([...resultEntries, ...skippedEntries], 'json'));
739
+ return;
740
+ }
741
+ const { active, connecting, failed } = printBulkConnectSummary(results, options);
742
+ if (active + connecting === 0 && failed > 0) {
646
743
  throw new ClientError(`Failed to connect any servers from ${configFile}`);
647
744
  }
648
745
  }
746
+ async function buildBulkConnectEntries(results, options) {
747
+ return await Promise.all(results.map(async (r) => {
748
+ if (r.status === 'failed') {
749
+ return {
750
+ _mcpc: {
751
+ sessionName: r.sessionName,
752
+ configFile: r.configFile,
753
+ entry: r.entry,
754
+ status: 'failed',
755
+ ...(r.error && { error: r.error }),
756
+ },
757
+ };
758
+ }
759
+ try {
760
+ return await buildConnectResultEntry(r.sessionName, r.status, {
761
+ ...(options.verbose && { verbose: options.verbose }),
762
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
763
+ configFile: r.configFile,
764
+ entry: r.entry,
765
+ });
766
+ }
767
+ catch (err) {
768
+ return {
769
+ _mcpc: {
770
+ sessionName: r.sessionName,
771
+ configFile: r.configFile,
772
+ entry: r.entry,
773
+ status: 'failed',
774
+ error: err instanceof Error ? err.message : String(err),
775
+ },
776
+ };
777
+ }
778
+ }));
779
+ }
780
+ function aggregateDiscoveredEntries(discovered, options) {
781
+ const entries = [];
782
+ const skippedDuplicates = [];
783
+ const skippedStdio = [];
784
+ const seenNames = new Set();
785
+ for (const d of discovered) {
786
+ for (const entry of Object.keys(d.config.mcpServers)) {
787
+ const sessionName = generateSessionName({ type: 'config', file: d.path, entry });
788
+ if (!options.stdio && isStdioEntry(d.config, entry)) {
789
+ skippedStdio.push({ configFile: d.path, entry, sessionName });
790
+ continue;
791
+ }
792
+ if (seenNames.has(sessionName)) {
793
+ skippedDuplicates.push({ configFile: d.path, entry, sessionName });
794
+ continue;
795
+ }
796
+ seenNames.add(sessionName);
797
+ entries.push({
798
+ configFile: d.path,
799
+ entry,
800
+ sessionName,
801
+ });
802
+ }
803
+ }
804
+ return { entries, skippedDuplicates, skippedStdio };
805
+ }
806
+ export async function connectAllFromStandardConfigs(options) {
807
+ const discovered = discoverMcpConfigFiles();
808
+ const hasApifyToken = !!process.env.APIFY_API_TOKEN;
809
+ if (discovered.length === 0 && !hasApifyToken) {
810
+ if (options.outputMode === 'json') {
811
+ console.log(formatOutput([], 'json'));
812
+ return;
813
+ }
814
+ const searchPaths = getStandardMcpConfigPaths()
815
+ .map((c) => ` ${c.path}`)
816
+ .join('\n');
817
+ throw new ClientError(`No MCP config files found in standard locations.\n\n` +
818
+ `Searched:\n${searchPaths}\n\n` +
819
+ `Connect a specific server: mcpc connect mcp.example.com\n` +
820
+ `Connect from a specific file: mcpc connect /path/to/mcp.json`);
821
+ }
822
+ if (discovered.length === 0) {
823
+ await maybeConnectApify([], [], options);
824
+ return;
825
+ }
826
+ const { entries, skippedDuplicates, skippedStdio } = aggregateDiscoveredEntries(discovered, {
827
+ ...(options.stdio && { stdio: true }),
828
+ });
829
+ if (options.outputMode === 'human') {
830
+ const totalEntries = entries.length + skippedDuplicates.length + skippedStdio.length;
831
+ console.log(theme.cyan(`Found ${discovered.length} MCP config file${discovered.length === 1 ? '' : 's'} ` +
832
+ `with ${totalEntries} server${totalEntries === 1 ? '' : 's'}:`));
833
+ for (const d of discovered) {
834
+ console.log(` ${d.path} ${chalk.dim(`(${d.serverCount} server${d.serverCount === 1 ? '' : 's'})`)}`);
835
+ for (const entryName of Object.keys(d.config.mcpServers)) {
836
+ const sessionName = generateSessionName({ type: 'config', file: d.path, entry: entryName });
837
+ const serverCfg = d.config.mcpServers[entryName];
838
+ const target = serverCfg?.url ?? [serverCfg?.command, ...(serverCfg?.args ?? [])].join(' ');
839
+ const truncated = target && target.length > 72 ? target.slice(0, 72) + '…' : target;
840
+ const isStdio = skippedStdio.some((s) => s.configFile === d.path && s.entry === entryName);
841
+ const isDuplicate = skippedDuplicates.some((s) => s.configFile === d.path && s.entry === entryName);
842
+ if (isStdio) {
843
+ console.log(` ${theme.cyan(sessionName)} → ${chalk.dim(truncated ?? entryName)} ${theme.yellow('○ skipped (stdio)')}`);
844
+ }
845
+ else if (isDuplicate) {
846
+ console.log(` ${theme.cyan(sessionName)} → ${chalk.dim(truncated ?? entryName)} ${chalk.dim('○ skipped (duplicate)')}`);
847
+ }
848
+ else {
849
+ console.log(` ${theme.cyan(sessionName)} → ${chalk.dim(truncated ?? entryName)}`);
850
+ }
851
+ }
852
+ }
853
+ if (entries.length === 0 && !hasApifyToken) {
854
+ throw new ClientError(`All servers in discovered config files use stdio transport.\n` +
855
+ `Pass --stdio to include them: mcpc connect --stdio`);
856
+ }
857
+ const parts = [];
858
+ if (entries.length > 0) {
859
+ parts.push(`Connecting ${entries.length} server${entries.length === 1 ? '' : 's'}`);
860
+ }
861
+ if (skippedStdio.length > 0) {
862
+ parts.push(`skipped ${skippedStdio.length} stdio server${skippedStdio.length === 1 ? '' : 's'}, pass --stdio to include`);
863
+ }
864
+ if (parts.length > 0) {
865
+ console.log(theme.cyan(`\n${parts.join('. ')}.`));
866
+ }
867
+ }
868
+ const skippedJsonEntries = [
869
+ ...skippedStdio.map((s) => ({
870
+ _mcpc: {
871
+ sessionName: s.sessionName,
872
+ configFile: s.configFile,
873
+ entry: s.entry,
874
+ status: 'skipped',
875
+ skipReason: 'stdio',
876
+ },
877
+ })),
878
+ ...skippedDuplicates.map((s) => ({
879
+ _mcpc: {
880
+ sessionName: s.sessionName,
881
+ configFile: s.configFile,
882
+ entry: s.entry,
883
+ status: 'skipped',
884
+ skipReason: 'duplicate',
885
+ },
886
+ })),
887
+ ];
888
+ if (entries.length === 0) {
889
+ if (!hasApifyToken && options.outputMode === 'json') {
890
+ console.log(formatOutput(skippedJsonEntries, 'json'));
891
+ return;
892
+ }
893
+ await maybeConnectApify([], [], options);
894
+ return;
895
+ }
896
+ const results = await bulkConnectEntries(entries, options);
897
+ if (options.outputMode === 'json') {
898
+ const resultEntries = await buildBulkConnectEntries(results, options);
899
+ console.log(formatOutput([...resultEntries, ...skippedJsonEntries], 'json'));
900
+ return;
901
+ }
902
+ const { active, connecting, failed } = printBulkConnectSummary(results, options);
903
+ await maybeConnectApify(entries, results, options);
904
+ if (active + connecting === 0 && failed > 0) {
905
+ throw new ClientError(`Failed to connect any servers from discovered config files`);
906
+ }
907
+ }
908
+ const APIFY_MCP_URL = 'https://mcp.apify.com';
909
+ const APIFY_SESSION_NAME = '@apify';
910
+ async function maybeConnectApify(configEntries, configResults, options) {
911
+ const token = process.env.APIFY_API_TOKEN;
912
+ if (!token)
913
+ return;
914
+ if (configEntries.some((e) => e.sessionName === APIFY_SESSION_NAME))
915
+ return;
916
+ if (configResults.some((r) => r.sessionName === APIFY_SESSION_NAME))
917
+ return;
918
+ const existing = await getSession(APIFY_SESSION_NAME);
919
+ const isLive = existing && getBridgeStatus(existing) === 'live';
920
+ if (options.outputMode === 'human') {
921
+ console.log(theme.cyan(`\nAPIFY_API_TOKEN detected, connecting to ${APIFY_MCP_URL}...`));
922
+ }
923
+ if (isLive) {
924
+ if (options.outputMode === 'human') {
925
+ console.log(` ${theme.green('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${chalk.dim('already active')}`);
926
+ }
927
+ return;
928
+ }
929
+ try {
930
+ await connectSession(APIFY_MCP_URL, APIFY_SESSION_NAME, {
931
+ outputMode: options.outputMode,
932
+ ...(options.verbose && { verbose: true }),
933
+ headers: [`Authorization: Bearer ${token}`],
934
+ skipDetails: true,
935
+ quiet: true,
936
+ noProfile: true,
937
+ });
938
+ if (options.outputMode === 'human') {
939
+ console.log(` ${theme.yellow('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${theme.yellow('connecting')}`);
940
+ }
941
+ }
942
+ catch (error) {
943
+ const msg = error instanceof Error ? error.message : String(error);
944
+ if (options.outputMode === 'human') {
945
+ console.log(` ${theme.red('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${theme.red('failed')}${chalk.dim(` — ${msg}`)}`);
946
+ }
947
+ }
948
+ }
649
949
  export async function openShell(target) {
650
950
  const { startShell } = await import('../shell.js');
651
951
  await startShell(target);