@apify/mcpc 0.2.4 → 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 (123) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/CONTRIBUTING.md +12 -0
  3. package/NOTICE +27 -0
  4. package/README.md +219 -226
  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 +51 -4
  9. package/dist/bridge/index.js.map +1 -1
  10. package/dist/cli/commands/auth.d.ts +2 -0
  11. package/dist/cli/commands/auth.d.ts.map +1 -1
  12. package/dist/cli/commands/auth.js +32 -10
  13. package/dist/cli/commands/auth.js.map +1 -1
  14. package/dist/cli/commands/clean.d.ts.map +1 -1
  15. package/dist/cli/commands/clean.js +13 -2
  16. package/dist/cli/commands/clean.js.map +1 -1
  17. package/dist/cli/commands/grep.d.ts.map +1 -1
  18. package/dist/cli/commands/grep.js +39 -8
  19. package/dist/cli/commands/grep.js.map +1 -1
  20. package/dist/cli/commands/prompts.d.ts.map +1 -1
  21. package/dist/cli/commands/prompts.js +7 -26
  22. package/dist/cli/commands/prompts.js.map +1 -1
  23. package/dist/cli/commands/resources.d.ts.map +1 -1
  24. package/dist/cli/commands/resources.js +9 -3
  25. package/dist/cli/commands/resources.js.map +1 -1
  26. package/dist/cli/commands/sessions.d.ts +45 -2
  27. package/dist/cli/commands/sessions.d.ts.map +1 -1
  28. package/dist/cli/commands/sessions.js +493 -27
  29. package/dist/cli/commands/sessions.js.map +1 -1
  30. package/dist/cli/commands/tasks.d.ts +1 -0
  31. package/dist/cli/commands/tasks.d.ts.map +1 -1
  32. package/dist/cli/commands/tasks.js +15 -1
  33. package/dist/cli/commands/tasks.js.map +1 -1
  34. package/dist/cli/commands/tools.d.ts +6 -1
  35. package/dist/cli/commands/tools.d.ts.map +1 -1
  36. package/dist/cli/commands/tools.js +66 -14
  37. package/dist/cli/commands/tools.js.map +1 -1
  38. package/dist/cli/commands/x402.d.ts.map +1 -1
  39. package/dist/cli/commands/x402.js +7 -7
  40. package/dist/cli/commands/x402.js.map +1 -1
  41. package/dist/cli/helpers.d.ts.map +1 -1
  42. package/dist/cli/helpers.js +3 -6
  43. package/dist/cli/helpers.js.map +1 -1
  44. package/dist/cli/index.js +370 -131
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/cli/output.d.ts +18 -5
  47. package/dist/cli/output.d.ts.map +1 -1
  48. package/dist/cli/output.js +275 -89
  49. package/dist/cli/output.js.map +1 -1
  50. package/dist/cli/parser.d.ts +4 -0
  51. package/dist/cli/parser.d.ts.map +1 -1
  52. package/dist/cli/parser.js +68 -24
  53. package/dist/cli/parser.js.map +1 -1
  54. package/dist/cli/shell.d.ts.map +1 -1
  55. package/dist/cli/shell.js +44 -21
  56. package/dist/cli/shell.js.map +1 -1
  57. package/dist/cli/tool-result.d.ts +1 -1
  58. package/dist/cli/tool-result.d.ts.map +1 -1
  59. package/dist/cli/tool-result.js +20 -15
  60. package/dist/cli/tool-result.js.map +1 -1
  61. package/dist/core/factory.d.ts +1 -0
  62. package/dist/core/factory.d.ts.map +1 -1
  63. package/dist/core/factory.js +3 -0
  64. package/dist/core/factory.js.map +1 -1
  65. package/dist/core/mcp-client.d.ts +1 -0
  66. package/dist/core/mcp-client.d.ts.map +1 -1
  67. package/dist/core/mcp-client.js +14 -0
  68. package/dist/core/mcp-client.js.map +1 -1
  69. package/dist/core/transports.d.ts +5 -1
  70. package/dist/core/transports.d.ts.map +1 -1
  71. package/dist/core/transports.js +26 -4
  72. package/dist/core/transports.js.map +1 -1
  73. package/dist/lib/auth/auth-page.d.ts +13 -0
  74. package/dist/lib/auth/auth-page.d.ts.map +1 -0
  75. package/dist/lib/auth/auth-page.js +129 -0
  76. package/dist/lib/auth/auth-page.js.map +1 -0
  77. package/dist/lib/auth/oauth-flow.d.ts +2 -1
  78. package/dist/lib/auth/oauth-flow.d.ts.map +1 -1
  79. package/dist/lib/auth/oauth-flow.js +65 -58
  80. package/dist/lib/auth/oauth-flow.js.map +1 -1
  81. package/dist/lib/auth/oauth-provider.d.ts +2 -0
  82. package/dist/lib/auth/oauth-provider.d.ts.map +1 -1
  83. package/dist/lib/auth/oauth-provider.js +6 -0
  84. package/dist/lib/auth/oauth-provider.js.map +1 -1
  85. package/dist/lib/auth/oauth-utils.d.ts +3 -0
  86. package/dist/lib/auth/oauth-utils.d.ts.map +1 -1
  87. package/dist/lib/auth/oauth-utils.js +32 -1
  88. package/dist/lib/auth/oauth-utils.js.map +1 -1
  89. package/dist/lib/auth/profiles.d.ts.map +1 -1
  90. package/dist/lib/auth/profiles.js +3 -3
  91. package/dist/lib/auth/profiles.js.map +1 -1
  92. package/dist/lib/bridge-manager.d.ts.map +1 -1
  93. package/dist/lib/bridge-manager.js +43 -28
  94. package/dist/lib/bridge-manager.js.map +1 -1
  95. package/dist/lib/cleanup.d.ts +5 -0
  96. package/dist/lib/cleanup.d.ts.map +1 -1
  97. package/dist/lib/cleanup.js +38 -1
  98. package/dist/lib/cleanup.js.map +1 -1
  99. package/dist/lib/config.d.ts +21 -0
  100. package/dist/lib/config.d.ts.map +1 -1
  101. package/dist/lib/config.js +99 -5
  102. package/dist/lib/config.js.map +1 -1
  103. package/dist/lib/errors.d.ts +1 -0
  104. package/dist/lib/errors.d.ts.map +1 -1
  105. package/dist/lib/errors.js +4 -1
  106. package/dist/lib/errors.js.map +1 -1
  107. package/dist/lib/session-client.d.ts +1 -0
  108. package/dist/lib/session-client.d.ts.map +1 -1
  109. package/dist/lib/session-client.js +7 -4
  110. package/dist/lib/session-client.js.map +1 -1
  111. package/dist/lib/sessions.d.ts.map +1 -1
  112. package/dist/lib/sessions.js +18 -9
  113. package/dist/lib/sessions.js.map +1 -1
  114. package/dist/lib/types.d.ts +2 -0
  115. package/dist/lib/types.d.ts.map +1 -1
  116. package/dist/lib/utils.d.ts +16 -2
  117. package/dist/lib/utils.d.ts.map +1 -1
  118. package/dist/lib/utils.js +112 -8
  119. package/dist/lib/utils.js.map +1 -1
  120. package/dist/lib/wallets.js +3 -3
  121. package/dist/lib/wallets.js.map +1 -1
  122. package/docs/TODOs.md +5 -0
  123. package/package.json +7 -6
@@ -1,10 +1,10 @@
1
1
  import { createServer } from 'net';
2
- import { isValidSessionName, 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, 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
- import { sessionExists, deleteSession, saveSession, updateSession, consolidateSessions, getSession, } from '../../lib/sessions.js';
7
+ import { sessionExists, deleteSession, saveSession, updateSession, consolidateSessions, getSession, loadSessions, } from '../../lib/sessions.js';
8
8
  import { startBridge, stopBridge, reconnectCrashedSessions, } from '../../lib/bridge-manager.js';
9
9
  import { storeKeychainSessionHeaders, storeKeychainProxyBearerToken, } from '../../lib/auth/keychain.js';
10
10
  import { AuthError, ClientError, isAuthenticationError, createServerAuthError, } from '../../lib/index.js';
@@ -12,6 +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, isStdioEntry, discoverMcpConfigFiles, getStandardMcpConfigPaths, } from '../../lib/config.js';
15
16
  const logger = createLogger('sessions');
16
17
  async function checkPortAvailable(host, port) {
17
18
  return new Promise((resolve) => {
@@ -32,6 +33,104 @@ async function checkPortAvailable(host, port) {
32
33
  server.listen(port, host);
33
34
  });
34
35
  }
36
+ async function findMatchingSession(parsed, options) {
37
+ const storage = await loadSessions();
38
+ const sessions = Object.values(storage.sessions);
39
+ if (sessions.length === 0)
40
+ return undefined;
41
+ const effectiveProfile = options.noProfile ? undefined : (options.profile ?? 'default');
42
+ for (const session of sessions) {
43
+ if (!session.server)
44
+ continue;
45
+ if (parsed.type === 'url') {
46
+ if (!session.server.url)
47
+ continue;
48
+ try {
49
+ const existingUrl = normalizeServerUrl(session.server.url);
50
+ const newUrl = normalizeServerUrl(parsed.url);
51
+ if (existingUrl !== newUrl)
52
+ continue;
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ }
58
+ else {
59
+ continue;
60
+ }
61
+ const sessionProfile = session.profileName ?? 'default';
62
+ if (effectiveProfile !== sessionProfile)
63
+ continue;
64
+ const existingHeaderKeys = Object.keys(session.server.headers || {}).sort();
65
+ const newHeaderKeys = (options.headers || [])
66
+ .map((h) => h.split(':')[0]?.trim() || '')
67
+ .filter(Boolean)
68
+ .sort();
69
+ if (existingHeaderKeys.join(',') !== newHeaderKeys.join(','))
70
+ continue;
71
+ return session.name;
72
+ }
73
+ return undefined;
74
+ }
75
+ export async function resolveSessionName(parsed, options) {
76
+ const existingName = await findMatchingSession(parsed, options);
77
+ if (existingName) {
78
+ return existingName;
79
+ }
80
+ const candidateName = generateSessionName(parsed);
81
+ const storage = await loadSessions();
82
+ if (!(candidateName in storage.sessions)) {
83
+ if (options.outputMode === 'human') {
84
+ console.log(theme.cyan(`Using session name: ${candidateName}`));
85
+ }
86
+ return candidateName;
87
+ }
88
+ for (let i = 2; i <= 99; i++) {
89
+ const suffixed = `${candidateName}-${i}`;
90
+ if (isValidSessionName(suffixed) && !(suffixed in storage.sessions)) {
91
+ if (options.outputMode === 'human') {
92
+ console.log(theme.cyan(`Using session name: ${suffixed}`));
93
+ }
94
+ return suffixed;
95
+ }
96
+ }
97
+ throw new ClientError(`Cannot auto-generate session name: too many sessions for this server.\n` +
98
+ `Specify a name explicitly: mcpc connect ${parsed.type === 'url' ? parsed.url : `${parsed.file}:${parsed.entry}`} @my-session`);
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
+ }
35
134
  export async function connectSession(target, name, options) {
36
135
  if (!isValidSessionName(name)) {
37
136
  throw new ClientError(`Invalid session name: ${name}\n` +
@@ -57,14 +156,25 @@ export async function connectSession(target, name, options) {
57
156
  if (existingSession) {
58
157
  const bridgeStatus = getBridgeStatus(existingSession);
59
158
  if (bridgeStatus === 'live') {
60
- if (options.outputMode === 'human') {
159
+ if (options.outputMode === 'human' && !options.quiet) {
61
160
  console.log(formatSuccess(`Session ${name} is already active`));
62
161
  }
63
- await showServerDetails(name, { ...options, hideTarget: false });
162
+ if (!options.skipDetails) {
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
+ }
173
+ }
64
174
  return;
65
175
  }
66
- if (options.outputMode === 'human') {
67
- console.log(chalk.yellow(`Session ${name} exists but bridge is ${bridgeStatus}, reconnecting...`));
176
+ if (options.outputMode === 'human' && !options.quiet) {
177
+ console.log(theme.yellow(`Session ${name} exists but bridge is ${bridgeStatus}, reconnecting...`));
68
178
  }
69
179
  try {
70
180
  await stopBridge(name);
@@ -184,23 +294,53 @@ export async function connectSession(target, name, options) {
184
294
  }
185
295
  throw error;
186
296
  }
187
- if (options.outputMode === 'human') {
188
- console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
297
+ if (options.skipDetails) {
298
+ if (options.outputMode === 'human' && !options.quiet) {
299
+ console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
300
+ }
301
+ return;
189
302
  }
190
303
  try {
191
- await showServerDetails(name, {
192
- ...options,
193
- hideTarget: false,
194
- });
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
+ });
316
+ console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
317
+ }
195
318
  }
196
319
  catch (detailsError) {
197
320
  if (detailsError instanceof AuthError) {
198
321
  throw detailsError;
199
322
  }
200
323
  if (detailsError instanceof Error && isAuthenticationError(detailsError.message)) {
201
- throw createServerAuthError(serverConfig.url || target, { sessionName: name });
324
+ const logPath = `${getLogsDir()}/bridge-${name}.log`;
325
+ throw createServerAuthError(serverConfig.url || target, { sessionName: name, logPath });
202
326
  }
203
- logger.debug(`showServerDetails failed for new session ${name}: ${detailsError.message}`);
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 {
339
+ console.log(formatWarning(`Session ${name} created but server is not responding: ${errorMsg}\n` +
340
+ ` The session will auto-recover when the server becomes available.\n` +
341
+ ` Check status with: mcpc ${name}`));
342
+ }
343
+ logger.debug(`showServerDetails failed for new session ${name}: ${errorMsg}`);
204
344
  }
205
345
  }
206
346
  export function getBridgeStatus(session) {
@@ -227,19 +367,19 @@ export function getBridgeStatus(session) {
227
367
  export function formatBridgeStatus(status) {
228
368
  switch (status) {
229
369
  case 'live':
230
- return { dot: chalk.green('●'), text: chalk.green('live') };
370
+ return { dot: theme.green('●'), text: theme.green('live') };
231
371
  case 'connecting':
232
- return { dot: chalk.yellow('●'), text: chalk.yellow('connecting') };
372
+ return { dot: theme.yellow('●'), text: theme.yellow('connecting') };
233
373
  case 'reconnecting':
234
- return { dot: chalk.yellow('●'), text: chalk.yellow('reconnecting') };
374
+ return { dot: theme.yellow('●'), text: theme.yellow('reconnecting') };
235
375
  case 'disconnected':
236
- return { dot: chalk.yellow('●'), text: chalk.yellow('disconnected') };
376
+ return { dot: theme.yellow('●'), text: theme.yellow('disconnected') };
237
377
  case 'crashed':
238
- return { dot: chalk.yellow('○'), text: chalk.yellow('crashed') };
378
+ return { dot: theme.yellow('○'), text: theme.yellow('crashed') };
239
379
  case 'unauthorized':
240
- return { dot: chalk.red('○'), text: chalk.red('unauthorized') };
380
+ return { dot: theme.red('○'), text: theme.red('unauthorized') };
241
381
  case 'expired':
242
- return { dot: chalk.red('○'), text: chalk.red('expired') };
382
+ return { dot: theme.red('○'), text: theme.red('expired') };
243
383
  }
244
384
  }
245
385
  export function formatTimeAgo(isoDate) {
@@ -323,7 +463,7 @@ export async function listSessionsAndAuthProfiles(options) {
323
463
  console.log(chalk.bold('Saved OAuth profiles:'));
324
464
  for (const profile of profiles) {
325
465
  const hostStr = getServerHost(profile.serverUrl);
326
- const nameStr = chalk.magenta(profile.name);
466
+ const nameStr = theme.magenta(profile.name);
327
467
  const userStr = profile.userEmail || profile.userName || '';
328
468
  const timeAgo = formatTimeAgo(profile.refreshedAt || profile.createdAt);
329
469
  const timeLabel = profile.refreshedAt ? 'refreshed' : 'created';
@@ -396,7 +536,7 @@ export async function showServerDetails(target, options) {
396
536
  capabilities,
397
537
  serverInfo,
398
538
  instructions,
399
- ...(tools.length > 0 && { tools }),
539
+ ...(tools.length > 0 && { toolNames: tools.map((t) => t.name) }),
400
540
  }, 'json'));
401
541
  }
402
542
  });
@@ -408,7 +548,7 @@ export async function restartSession(name, options) {
408
548
  throw new ClientError(`Session not found: ${name}`);
409
549
  }
410
550
  if (options.outputMode === 'human') {
411
- console.log(chalk.yellow(`Restarting session ${name}...`));
551
+ console.log(theme.yellow(`Restarting session ${name}...`));
412
552
  }
413
553
  try {
414
554
  await stopBridge(name);
@@ -457,6 +597,7 @@ export async function restartSession(name, options) {
457
597
  logger.debug(`Session ${name} restarted with bridge PID: ${pid}`);
458
598
  if (options.outputMode === 'human') {
459
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)'));
460
601
  }
461
602
  await showServerDetails(name, {
462
603
  ...options,
@@ -477,8 +618,333 @@ export async function restartSession(name, options) {
477
618
  throw error;
478
619
  }
479
620
  }
480
- export async function showHelp(target, options) {
481
- await showServerDetails(target, options);
621
+ async function bulkConnectEntries(entries, options) {
622
+ const liveSet = new Set();
623
+ for (const { sessionName } of entries) {
624
+ const session = await getSession(sessionName);
625
+ if (session && getBridgeStatus(session) === 'live') {
626
+ liveSet.add(sessionName);
627
+ }
628
+ }
629
+ const settled = await Promise.allSettled(entries.map(async ({ entry, sessionName, configFile }) => connectSession(entry, sessionName, {
630
+ ...options,
631
+ config: configFile,
632
+ skipDetails: true,
633
+ quiet: true,
634
+ })));
635
+ const results = settled.map((outcome, i) => {
636
+ const base = entries[i];
637
+ if (outcome.status === 'fulfilled') {
638
+ return { ...base, status: liveSet.has(base.sessionName) ? 'active' : 'created' };
639
+ }
640
+ const error = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
641
+ return { ...base, status: 'failed', error };
642
+ });
643
+ if (options.outputMode === 'human') {
644
+ for (const r of results) {
645
+ const name = theme.cyan(r.sessionName);
646
+ switch (r.status) {
647
+ case 'created':
648
+ console.log(` ${theme.yellow('●')} ${name} ${theme.yellow('connecting')}`);
649
+ break;
650
+ case 'active':
651
+ console.log(` ${theme.green('●')} ${name} ${chalk.dim('already active')}`);
652
+ break;
653
+ case 'failed':
654
+ console.log(` ${theme.red('●')} ${name} ${theme.red('failed')}${r.error ? chalk.dim(` — ${r.error}`) : ''}`);
655
+ break;
656
+ }
657
+ }
658
+ }
659
+ return results;
660
+ }
661
+ function printBulkConnectSummary(results, options) {
662
+ const active = results.filter((r) => r.status === 'active').length;
663
+ const connecting = results.filter((r) => r.status === 'created').length;
664
+ const failed = results.filter((r) => r.status === 'failed').length;
665
+ if (options.outputMode === 'human' && results.length > 1) {
666
+ const parts = [];
667
+ if (active > 0)
668
+ parts.push(`${active} already active`);
669
+ if (connecting > 0)
670
+ parts.push(`${connecting} connecting`);
671
+ if (failed > 0)
672
+ parts.push(`${failed} failed`);
673
+ const summary = parts.join(', ');
674
+ if (failed === 0) {
675
+ console.log(formatSuccess(summary));
676
+ }
677
+ else if (active + connecting > 0) {
678
+ console.log(formatWarning(summary));
679
+ }
680
+ }
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) {
743
+ throw new ClientError(`Failed to connect any servers from ${configFile}`);
744
+ }
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
+ }
482
948
  }
483
949
  export async function openShell(target) {
484
950
  const { startShell } = await import('../shell.js');