@apify/mcpc 0.3.1-beta.0 → 0.3.1

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.
@@ -1,352 +1,20 @@
1
- import { createServer } from 'net';
2
- import { stat } from 'fs/promises';
3
- import { isValidSessionName, generateSessionName, normalizeServerUrl, validateProfileName, isProcessAlive, getServerHost, redactHeaders, } from '../../lib/index.js';
1
+ import { isProcessAlive, getServerHost, redactHeaders, ClientError, } from '../../lib/index.js';
4
2
  import { DISCONNECTED_THRESHOLD_MS } from '../../lib/types.js';
5
- import { formatOutput, formatSuccess, formatWarning, formatError, formatSessionLine, formatServerDetails, formatPath, formatConnectStatusBadge, theme, } from '../output.js';
6
- import { withMcpClient, resolveTarget, resolveAuthProfile } from '../helpers.js';
3
+ import { formatOutput, formatSuccess, formatWarning, formatError, formatSessionLine, formatServerDetails, theme, } from '../output.js';
4
+ import { withMcpClient, resolveAuthProfile } from '../helpers.js';
7
5
  import { listAuthProfiles } from '../../lib/auth/profiles.js';
8
- import { sessionExists, deleteSession, saveSession, updateSession, consolidateSessions, getSession, loadSessions, } from '../../lib/sessions.js';
6
+ import { sessionExists, deleteSession, updateSession, consolidateSessions, getSession, } from '../../lib/sessions.js';
9
7
  import { startBridge, stopBridge, reconnectCrashedSessions, } from '../../lib/bridge-manager.js';
10
- import { storeKeychainSessionHeaders, storeKeychainProxyBearerToken, } from '../../lib/auth/keychain.js';
11
- import { AuthError, ClientError, isAuthenticationError, createServerAuthError, } from '../../lib/index.js';
12
- import { getWallet } from '../../lib/wallets.js';
13
8
  import chalk from 'chalk';
14
9
  import { createLogger } from '../../lib/logger.js';
15
- import { parseProxyArg } from '../parser.js';
16
10
  import { getBridgeLogPath } from '../../lib/log-reader.js';
17
- import { loadConfig, listServers, isStdioEntry, scanMcpConfigFiles, getStandardMcpConfigPaths, } from '../../lib/config.js';
18
11
  const logger = createLogger('sessions');
19
- async function checkPortAvailable(host, port) {
20
- return new Promise((resolve) => {
21
- const server = createServer();
22
- server.once('error', (err) => {
23
- if (err.code === 'EADDRINUSE') {
24
- resolve(false);
25
- }
26
- else {
27
- resolve(false);
28
- }
29
- });
30
- server.once('listening', () => {
31
- server.close(() => {
32
- resolve(true);
33
- });
34
- });
35
- server.listen(port, host);
36
- });
37
- }
38
- async function findMatchingSession(parsed, options) {
39
- const storage = await loadSessions();
40
- const sessions = Object.values(storage.sessions);
41
- if (sessions.length === 0)
42
- return undefined;
43
- const effectiveProfile = options.noProfile ? undefined : (options.profile ?? 'default');
44
- for (const session of sessions) {
45
- if (!session.server)
46
- continue;
47
- if (parsed.type === 'url') {
48
- if (!session.server.url)
49
- continue;
50
- try {
51
- const existingUrl = normalizeServerUrl(session.server.url);
52
- const newUrl = normalizeServerUrl(parsed.url);
53
- if (existingUrl !== newUrl)
54
- continue;
55
- }
56
- catch {
57
- continue;
58
- }
59
- }
60
- else {
61
- continue;
62
- }
63
- const sessionProfile = session.profileName ?? 'default';
64
- if (effectiveProfile !== sessionProfile)
65
- continue;
66
- const existingHeaderKeys = Object.keys(session.server.headers || {}).sort();
67
- const newHeaderKeys = (options.headers || [])
68
- .map((h) => h.split(':')[0]?.trim() || '')
69
- .filter(Boolean)
70
- .sort();
71
- if (existingHeaderKeys.join(',') !== newHeaderKeys.join(','))
72
- continue;
73
- return session.name;
74
- }
75
- return undefined;
76
- }
77
- export async function resolveSessionName(parsed, options) {
78
- const existingName = await findMatchingSession(parsed, options);
79
- if (existingName) {
80
- return existingName;
81
- }
82
- const candidateName = generateSessionName(parsed);
83
- const storage = await loadSessions();
84
- if (!(candidateName in storage.sessions)) {
85
- if (options.outputMode === 'human') {
86
- console.log(theme.cyan(`Using session name: ${candidateName}`));
87
- }
88
- return candidateName;
89
- }
90
- for (let i = 2; i <= 99; i++) {
91
- const suffixed = `${candidateName}-${i}`;
92
- if (isValidSessionName(suffixed) && !(suffixed in storage.sessions)) {
93
- if (options.outputMode === 'human') {
94
- console.log(theme.cyan(`Using session name: ${suffixed}`));
95
- }
96
- return suffixed;
97
- }
98
- }
99
- throw new ClientError(`Cannot auto-generate session name: too many sessions for this server.\n` +
100
- `Specify a name explicitly: mcpc connect ${parsed.type === 'url' ? parsed.url : `${parsed.file}:${parsed.entry}`} @my-session`);
101
- }
102
- async function buildConnectResultEntry(sessionName, status, options) {
103
- return await withMcpClient(sessionName, {
104
- outputMode: 'json',
105
- hideTarget: true,
106
- ...(options.verbose && { verbose: options.verbose }),
107
- ...(options.timeout !== undefined && { timeout: options.timeout }),
108
- }, async (client, context) => {
109
- const serverDetails = await client.getServerDetails();
110
- const tools = (await client.listAllTools()).tools;
111
- const server = context.serverConfig
112
- ? {
113
- ...context.serverConfig,
114
- ...(context.serverConfig.headers && {
115
- headers: redactHeaders(context.serverConfig.headers),
116
- }),
117
- }
118
- : undefined;
119
- return {
120
- _mcpc: {
121
- sessionName: context.sessionName ?? sessionName,
122
- ...(context.profileName && { profileName: context.profileName }),
123
- ...(server && { server }),
124
- ...(options.configFile && { configFile: options.configFile }),
125
- ...(options.entry && { entry: options.entry }),
126
- status,
127
- ...(serverDetails.statefulness &&
128
- serverDetails.statefulness !== 'unknown' && {
129
- statefulness: serverDetails.statefulness,
130
- }),
131
- },
132
- ...(serverDetails.protocolVersion && { protocolVersion: serverDetails.protocolVersion }),
133
- ...(serverDetails.capabilities && { capabilities: serverDetails.capabilities }),
134
- ...(serverDetails.serverInfo && { serverInfo: serverDetails.serverInfo }),
135
- ...(serverDetails.instructions && { instructions: serverDetails.instructions }),
136
- ...(tools.length > 0 && { toolNames: tools.map((t) => t.name) }),
137
- };
138
- });
139
- }
140
- export async function connectSession(target, name, options) {
141
- if (!isValidSessionName(name)) {
142
- throw new ClientError(`Invalid session name: ${name}\n` +
143
- `Session names must start with @ and be followed by 1-64 characters, alphanumeric with hyphens or underscores only (e.g., @my-session).`);
144
- }
145
- if (options.profile) {
146
- validateProfileName(options.profile);
147
- }
148
- let proxyConfig;
149
- if (options.proxy) {
150
- proxyConfig = parseProxyArg(options.proxy);
151
- logger.debug(`Proxy config: ${proxyConfig.host}:${proxyConfig.port}`);
152
- const portAvailable = await checkPortAvailable(proxyConfig.host, proxyConfig.port);
153
- if (!portAvailable) {
154
- throw new ClientError(`Port ${proxyConfig.port} is already in use on ${proxyConfig.host}. ` +
155
- `Choose a different port with --proxy [host:]port`);
156
- }
157
- }
158
- if (options.proxyBearerToken && !options.proxy) {
159
- throw new ClientError('--proxy-bearer-token requires --proxy to be specified');
160
- }
161
- const existingSession = await getSession(name);
162
- if (existingSession) {
163
- const bridgeStatus = getBridgeStatus(existingSession);
164
- if (bridgeStatus === 'live') {
165
- if (options.outputMode === 'human' && !options.quiet) {
166
- console.log(formatSuccess(`Session ${name} is already active`));
167
- }
168
- if (!options.skipDetails) {
169
- if (options.outputMode === 'json') {
170
- const entry = await buildConnectResultEntry(name, 'active', {
171
- ...(options.verbose && { verbose: options.verbose }),
172
- ...(options.timeout !== undefined && { timeout: options.timeout }),
173
- });
174
- console.log(formatOutput([entry], 'json'));
175
- }
176
- else {
177
- await showServerDetails(name, { ...options, hideTarget: false });
178
- }
179
- }
180
- return;
181
- }
182
- if (options.outputMode === 'human' && !options.quiet) {
183
- console.log(theme.yellow(`Session ${name} exists but bridge is ${bridgeStatus}, reconnecting...`));
184
- }
185
- try {
186
- await stopBridge(name);
187
- }
188
- catch {
189
- }
190
- }
191
- const serverConfig = await resolveTarget(target, options);
192
- const hasExplicitAuthHeader = serverConfig.headers?.Authorization !== undefined;
193
- const hasExplicitProfile = options.profile !== undefined;
194
- if (hasExplicitAuthHeader && hasExplicitProfile) {
195
- throw new ClientError(`Cannot combine --profile with --header "Authorization: ...".\n\n` +
196
- `Use either:\n` +
197
- ` --profile ${options.profile} (OAuth authentication via saved profile)\n` +
198
- ` --header "Authorization: Bearer <token>" (static bearer token)`);
199
- }
200
- let profileName;
201
- if (serverConfig.url) {
202
- if (options.noProfile) {
203
- logger.debug('Skipping OAuth profile: --no-profile specified');
204
- }
205
- else if (hasExplicitAuthHeader) {
206
- logger.debug('Skipping OAuth profile auto-detection: explicit Authorization header provided via --header');
207
- }
208
- else if (options.x402 && !options.profile) {
209
- logger.debug('Skipping OAuth profile auto-detection: --x402 specified');
210
- }
211
- else {
212
- profileName = await resolveAuthProfile(serverConfig.url, target, options.profile, {
213
- sessionName: name,
214
- });
215
- }
216
- }
217
- let headers;
218
- if (Object.keys(serverConfig.headers || {}).length > 0) {
219
- headers = { ...serverConfig.headers };
220
- if (Object.keys(headers).length > 0) {
221
- logger.debug(`Storing ${Object.keys(headers).length} headers for session ${name} in keychain`);
222
- await storeKeychainSessionHeaders(name, headers);
223
- }
224
- else {
225
- headers = undefined;
226
- }
227
- }
228
- if (options.proxyBearerToken) {
229
- logger.debug(`Storing proxy bearer token for session ${name} in keychain`);
230
- await storeKeychainProxyBearerToken(name, options.proxyBearerToken);
231
- }
232
- if (options.x402) {
233
- const wallet = await getWallet();
234
- if (!wallet) {
235
- throw new ClientError('x402 wallet not found. Create one with: mcpc x402 init');
236
- }
237
- logger.debug(`Using x402 wallet: ${wallet.address}`);
238
- }
239
- const isReconnect = !!existingSession;
240
- const { headers: _originalHeaders, ...baseTransportConfig } = serverConfig;
241
- const sessionTransportConfig = {
242
- ...baseTransportConfig,
243
- ...(headers && { headers: redactHeaders(headers) }),
244
- };
245
- const sessionUpdate = {
246
- server: sessionTransportConfig,
247
- ...(profileName && { profileName }),
248
- ...(proxyConfig && { proxy: proxyConfig }),
249
- ...(options.x402 && { x402: options.x402 }),
250
- ...(options.insecure && { insecure: true }),
251
- ...(isReconnect && { status: 'active' }),
252
- };
253
- if (isReconnect) {
254
- await updateSession(name, sessionUpdate);
255
- logger.debug(`Session record updated for reconnect: ${name}`);
256
- }
257
- else {
258
- await saveSession(name, {
259
- server: sessionTransportConfig,
260
- createdAt: new Date().toISOString(),
261
- status: 'connecting',
262
- lastConnectionAttemptAt: new Date().toISOString(),
263
- ...sessionUpdate,
264
- });
265
- logger.debug(`Initial session record created for: ${name}`);
266
- }
267
- try {
268
- const bridgeOptions = {
269
- sessionName: name,
270
- serverConfig: serverConfig,
271
- verbose: options.verbose || false,
272
- };
273
- if (headers) {
274
- bridgeOptions.headers = headers;
275
- }
276
- if (profileName) {
277
- bridgeOptions.profileName = profileName;
278
- }
279
- if (proxyConfig) {
280
- bridgeOptions.proxyConfig = proxyConfig;
281
- }
282
- if (options.x402) {
283
- bridgeOptions.x402 = options.x402;
284
- }
285
- if (options.insecure) {
286
- bridgeOptions.insecure = true;
287
- }
288
- const { pid } = await startBridge(bridgeOptions);
289
- await updateSession(name, { pid, status: 'active' });
290
- logger.debug(`Session ${name} updated with bridge PID: ${pid}`);
291
- }
292
- catch (error) {
293
- logger.debug(`Bridge start failed, cleaning up session ${name}`);
294
- if (!isReconnect) {
295
- try {
296
- await deleteSession(name);
297
- }
298
- catch {
299
- }
300
- }
301
- throw error;
302
- }
303
- if (options.skipDetails) {
304
- if (options.outputMode === 'human' && !options.quiet) {
305
- console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
306
- }
307
- return;
308
- }
309
- try {
310
- if (options.outputMode === 'json') {
311
- const entry = await buildConnectResultEntry(name, 'created', {
312
- ...(options.verbose && { verbose: options.verbose }),
313
- ...(options.timeout !== undefined && { timeout: options.timeout }),
314
- });
315
- console.log(formatOutput([entry], 'json'));
316
- }
317
- else {
318
- await showServerDetails(name, {
319
- ...options,
320
- hideTarget: false,
321
- });
322
- console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
323
- }
324
- }
325
- catch (detailsError) {
326
- if (detailsError instanceof AuthError) {
327
- throw detailsError;
328
- }
329
- if (detailsError instanceof Error && isAuthenticationError(detailsError.message)) {
330
- throw createServerAuthError(serverConfig.url || target, { sessionName: name });
331
- }
332
- const errorMsg = detailsError instanceof Error ? detailsError.message : String(detailsError);
333
- if (options.outputMode === 'json') {
334
- const failed = {
335
- _mcpc: {
336
- sessionName: name,
337
- status: 'failed',
338
- error: errorMsg,
339
- },
340
- };
341
- console.log(formatOutput([failed], 'json'));
342
- }
343
- else {
344
- console.log(formatWarning(`Session ${name} created but server is not responding: ${errorMsg}\n` +
345
- ` The session will auto-recover when the server becomes available.\n` +
346
- ` Check status with: mcpc ${name}`));
347
- }
348
- logger.debug(`showServerDetails failed for new session ${name}: ${errorMsg}`);
349
- }
12
+ export function statelessField(connectionMode) {
13
+ if (connectionMode === 'stateless')
14
+ return { stateless: true };
15
+ if (connectionMode === 'stateful')
16
+ return { stateless: false };
17
+ return { stateless: null };
350
18
  }
351
19
  export function getBridgeStatus(session) {
352
20
  if (session.status === 'unauthorized') {
@@ -420,9 +88,10 @@ export async function listSessionsAndAuthProfiles(options) {
420
88
  reconnectCrashedSessions(consolidateResult.sessionsToRestart);
421
89
  const profiles = await listAuthProfiles();
422
90
  if (options.outputMode === 'json') {
423
- const sessionsWithStatus = sessions.map((session) => ({
91
+ const sessionsWithStatus = sessions.map(({ connectionMode, ...session }) => ({
424
92
  ...session,
425
93
  status: getBridgeStatus(session),
94
+ ...statelessField(connectionMode),
426
95
  }));
427
96
  console.log(formatOutput({
428
97
  sessions: sessionsWithStatus,
@@ -522,7 +191,7 @@ export async function closeSession(name, options) {
522
191
  export async function showServerDetails(target, options) {
523
192
  await withMcpClient(target, options, async (client, context) => {
524
193
  const serverDetails = await client.getServerDetails();
525
- const { serverInfo, capabilities, instructions, protocolVersion, statefulness } = serverDetails;
194
+ const { serverInfo, capabilities, instructions, protocolVersion, connectionMode } = serverDetails;
526
195
  const cachedToolsResult = await client.listAllTools();
527
196
  const tools = cachedToolsResult.tools;
528
197
  if (options.outputMode === 'human') {
@@ -536,24 +205,16 @@ export async function showServerDetails(target, options) {
536
205
  }),
537
206
  };
538
207
  let logPath;
539
- let logSize;
540
208
  if (target.startsWith('@')) {
541
209
  logPath = getBridgeLogPath(target);
542
- try {
543
- const st = await stat(logPath);
544
- logSize = st.size;
545
- }
546
- catch {
547
- }
548
210
  }
549
211
  console.log(formatOutput({
550
212
  _mcpc: {
551
213
  sessionName: context.sessionName,
552
214
  profileName: context.profileName,
553
215
  server,
554
- ...(statefulness && statefulness !== 'unknown' && { statefulness }),
216
+ ...statelessField(connectionMode),
555
217
  ...(logPath && { logPath }),
556
- ...(logSize !== undefined && { logSize }),
557
218
  },
558
219
  protocolVersion,
559
220
  capabilities,
@@ -584,14 +245,6 @@ export async function restartSession(name, options) {
584
245
  }
585
246
  const { readKeychainSessionHeaders } = await import('../../lib/auth/keychain.js');
586
247
  const headers = await readKeychainSessionHeaders(name);
587
- const bridgeOptions = {
588
- sessionName: name,
589
- serverConfig: { ...serverConfig, ...(headers && { headers }) },
590
- verbose: options.verbose || false,
591
- };
592
- if (headers) {
593
- bridgeOptions.headers = headers;
594
- }
595
248
  const hasExplicitAuthHeader = headers?.Authorization !== undefined;
596
249
  let profileName = session.profileName;
597
250
  if (!profileName && serverConfig.url && !hasExplicitAuthHeader && !session.x402) {
@@ -603,18 +256,16 @@ export async function restartSession(name, options) {
603
256
  await updateSession(name, { profileName });
604
257
  }
605
258
  }
606
- if (profileName) {
607
- bridgeOptions.profileName = profileName;
608
- }
609
- if (session.proxy) {
610
- bridgeOptions.proxyConfig = session.proxy;
611
- }
612
- if (session.x402) {
613
- bridgeOptions.x402 = session.x402;
614
- }
615
- if (session.insecure) {
616
- bridgeOptions.insecure = session.insecure;
617
- }
259
+ const bridgeOptions = {
260
+ sessionName: name,
261
+ serverConfig: { ...serverConfig, ...(headers && { headers }) },
262
+ verbose: options.verbose || false,
263
+ ...(headers && { headers }),
264
+ ...(profileName && { profileName }),
265
+ ...(session.proxy && { proxyConfig: session.proxy }),
266
+ ...(session.x402 && { x402: session.x402 }),
267
+ ...(session.insecure && { insecure: session.insecure }),
268
+ };
618
269
  const { pid } = await startBridge(bridgeOptions);
619
270
  await updateSession(name, { pid, status: 'active' });
620
271
  logger.debug(`Session ${name} restarted with bridge PID: ${pid}`);
@@ -641,366 +292,6 @@ export async function restartSession(name, options) {
641
292
  throw error;
642
293
  }
643
294
  }
644
- async function bulkConnectEntries(entries, options, { printBadges = true } = {}) {
645
- const liveSet = new Set();
646
- for (const { sessionName } of entries) {
647
- const session = await getSession(sessionName);
648
- if (session && getBridgeStatus(session) === 'live') {
649
- liveSet.add(sessionName);
650
- }
651
- }
652
- const settled = await Promise.allSettled(entries.map(async ({ entry, sessionName, configFile }) => connectSession(entry, sessionName, {
653
- ...options,
654
- config: configFile,
655
- skipDetails: true,
656
- quiet: true,
657
- })));
658
- const results = settled.map((outcome, i) => {
659
- const base = entries[i];
660
- if (outcome.status === 'fulfilled') {
661
- return { ...base, status: liveSet.has(base.sessionName) ? 'active' : 'created' };
662
- }
663
- const error = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
664
- return { ...base, status: 'failed', error };
665
- });
666
- if (options.outputMode === 'human' && printBadges) {
667
- for (const r of results) {
668
- const name = theme.cyan(r.sessionName);
669
- switch (r.status) {
670
- case 'created':
671
- console.log(` ${theme.yellow('●')} ${name} ${theme.yellow('connecting')}`);
672
- break;
673
- case 'active':
674
- console.log(` ${theme.green('●')} ${name} ${chalk.dim('already active')}`);
675
- break;
676
- case 'failed':
677
- console.log(` ${theme.red('●')} ${name} ${theme.red('failed')}${r.error ? chalk.dim(` — ${r.error}`) : ''}`);
678
- break;
679
- }
680
- }
681
- }
682
- return results;
683
- }
684
- function printBulkConnectSummary(results, options) {
685
- const active = results.filter((r) => r.status === 'active').length;
686
- const connecting = results.filter((r) => r.status === 'created').length;
687
- const failed = results.filter((r) => r.status === 'failed').length;
688
- if (options.outputMode === 'human' && results.length > 1) {
689
- const parts = [];
690
- if (active > 0)
691
- parts.push(`${active} already active`);
692
- if (connecting > 0)
693
- parts.push(`${connecting} connecting`);
694
- if (failed > 0)
695
- parts.push(`${failed} failed`);
696
- const summary = parts.join(', ');
697
- if (failed === 0) {
698
- console.log(formatSuccess(summary));
699
- }
700
- else if (active + connecting > 0) {
701
- console.log(formatWarning(summary));
702
- }
703
- }
704
- return { active, connecting, failed };
705
- }
706
- export async function connectAllFromConfig(configFile, options) {
707
- const config = loadConfig(configFile);
708
- const allNames = listServers(config);
709
- if (allNames.length === 0) {
710
- throw new ClientError(`No servers found in config file: ${configFile}`);
711
- }
712
- const stdioSkipped = [];
713
- const serverNames = allNames.filter((name) => {
714
- if (!options.stdio && isStdioEntry(config, name)) {
715
- stdioSkipped.push(name);
716
- return false;
717
- }
718
- return true;
719
- });
720
- if (serverNames.length === 0) {
721
- if (options.outputMode === 'json') {
722
- const skippedEntries = stdioSkipped.map((entry) => ({
723
- _mcpc: {
724
- sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
725
- configFile,
726
- entry,
727
- status: 'skipped',
728
- skipReason: 'stdio',
729
- },
730
- }));
731
- console.log(formatOutput(skippedEntries, 'json'));
732
- return;
733
- }
734
- throw new ClientError(`All ${allNames.length} server${allNames.length === 1 ? '' : 's'} in ${configFile} use stdio transport.\n` +
735
- `Pass --stdio to include them: mcpc connect ${configFile} --stdio`);
736
- }
737
- if (options.outputMode === 'human') {
738
- console.log(theme.cyan(`Connecting ${serverNames.length} server${serverNames.length === 1 ? '' : 's'} from ${configFile}...`));
739
- if (stdioSkipped.length > 0) {
740
- console.log(chalk.dim(` skipping ${stdioSkipped.length} stdio server${stdioSkipped.length === 1 ? '' : 's'} ` +
741
- `(${stdioSkipped.join(', ')}), pass --stdio to include`));
742
- }
743
- }
744
- const entries = serverNames.map((entry) => ({
745
- configFile,
746
- entry,
747
- sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
748
- }));
749
- const results = await bulkConnectEntries(entries, options);
750
- if (options.outputMode === 'json') {
751
- const resultEntries = await buildBulkConnectEntries(results, options);
752
- const skippedEntries = stdioSkipped.map((entry) => ({
753
- _mcpc: {
754
- sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
755
- configFile,
756
- entry,
757
- status: 'skipped',
758
- skipReason: 'stdio',
759
- },
760
- }));
761
- console.log(formatOutput([...resultEntries, ...skippedEntries], 'json'));
762
- return;
763
- }
764
- const { active, connecting, failed } = printBulkConnectSummary(results, options);
765
- if (active + connecting === 0 && failed > 0) {
766
- throw new ClientError(`Failed to connect any servers from ${configFile}`);
767
- }
768
- }
769
- async function buildBulkConnectEntries(results, options) {
770
- return await Promise.all(results.map(async (r) => {
771
- if (r.status === 'failed') {
772
- return {
773
- _mcpc: {
774
- sessionName: r.sessionName,
775
- configFile: r.configFile,
776
- entry: r.entry,
777
- status: 'failed',
778
- ...(r.error && { error: r.error }),
779
- },
780
- };
781
- }
782
- try {
783
- return await buildConnectResultEntry(r.sessionName, r.status, {
784
- ...(options.verbose && { verbose: options.verbose }),
785
- ...(options.timeout !== undefined && { timeout: options.timeout }),
786
- configFile: r.configFile,
787
- entry: r.entry,
788
- });
789
- }
790
- catch (err) {
791
- return {
792
- _mcpc: {
793
- sessionName: r.sessionName,
794
- configFile: r.configFile,
795
- entry: r.entry,
796
- status: 'failed',
797
- error: err instanceof Error ? err.message : String(err),
798
- },
799
- };
800
- }
801
- }));
802
- }
803
- function aggregateDiscoveredEntries(discovered, options) {
804
- const entries = [];
805
- const skippedDuplicates = [];
806
- const skippedStdio = [];
807
- const seenNames = new Set();
808
- for (const d of discovered) {
809
- for (const entry of Object.keys(d.config.mcpServers)) {
810
- const sessionName = generateSessionName({ type: 'config', file: d.path, entry });
811
- if (!options.stdio && isStdioEntry(d.config, entry)) {
812
- skippedStdio.push({ configFile: d.path, entry, sessionName });
813
- continue;
814
- }
815
- if (seenNames.has(sessionName)) {
816
- skippedDuplicates.push({ configFile: d.path, entry, sessionName });
817
- continue;
818
- }
819
- seenNames.add(sessionName);
820
- entries.push({
821
- configFile: d.path,
822
- entry,
823
- sessionName,
824
- });
825
- }
826
- }
827
- return { entries, skippedDuplicates, skippedStdio };
828
- }
829
- function buildNoServersError(scan) {
830
- if (scan.empty.length > 0 || scan.errors.length > 0) {
831
- const lines = [];
832
- if (scan.empty.length > 0) {
833
- lines.push(scan.empty.length === 1
834
- ? `Found a config file, but it defines no servers:`
835
- : `Found config files, but they define no servers:`);
836
- for (const c of scan.empty)
837
- lines.push(` ${formatPath(c.path)}`);
838
- }
839
- if (scan.errors.length > 0) {
840
- if (lines.length > 0)
841
- lines.push('');
842
- lines.push(scan.errors.length === 1
843
- ? `Found a config file, but it couldn't be used:`
844
- : `Found config files, but they couldn't be used:`);
845
- for (const c of scan.errors)
846
- lines.push(` ${formatPath(c.path)} — ${c.error}`);
847
- }
848
- return (`No MCP servers to connect.\n\n` +
849
- `${lines.join('\n')}\n\n` +
850
- `Add a server under "mcpServers" and re-run mcpc connect, or connect one now:\n` +
851
- ` mcpc connect mcp.example.com @myserver`);
852
- }
853
- const searchPaths = getStandardMcpConfigPaths()
854
- .map((c) => ` ${formatPath(c.path)}`)
855
- .join('\n');
856
- return (`No MCP config files found in standard locations.\n\n` +
857
- `Searched:\n${searchPaths}\n\n` +
858
- `Connect a specific server: mcpc connect mcp.example.com\n` +
859
- `Connect from a specific file: mcpc connect /path/to/mcp.json`);
860
- }
861
- export async function connectAllFromStandardConfigs(options) {
862
- const scan = scanMcpConfigFiles();
863
- const { discovered } = scan;
864
- const hasApifyToken = !!process.env.APIFY_API_TOKEN;
865
- if (discovered.length === 0 && !hasApifyToken) {
866
- if (options.outputMode === 'json') {
867
- console.log(formatOutput([], 'json'));
868
- return;
869
- }
870
- throw new ClientError(buildNoServersError(scan));
871
- }
872
- if (discovered.length === 0) {
873
- await maybeConnectApify([], [], options);
874
- return;
875
- }
876
- const { entries, skippedDuplicates, skippedStdio } = aggregateDiscoveredEntries(discovered, {
877
- ...(options.stdio && { stdio: true }),
878
- });
879
- const results = entries.length > 0 ? await bulkConnectEntries(entries, options, { printBadges: false }) : [];
880
- if (options.outputMode === 'json') {
881
- const toSkipped = (s, skipReason) => ({
882
- _mcpc: {
883
- sessionName: s.sessionName,
884
- configFile: s.configFile,
885
- entry: s.entry,
886
- status: 'skipped',
887
- skipReason,
888
- },
889
- });
890
- const skippedJsonEntries = [
891
- ...skippedStdio.map((s) => toSkipped(s, 'stdio')),
892
- ...skippedDuplicates.map((s) => toSkipped(s, 'duplicate')),
893
- ];
894
- if (entries.length === 0) {
895
- if (!hasApifyToken) {
896
- console.log(formatOutput(skippedJsonEntries, 'json'));
897
- return;
898
- }
899
- await maybeConnectApify([], [], options);
900
- return;
901
- }
902
- const resultEntries = await buildBulkConnectEntries(results, options);
903
- console.log(formatOutput([...resultEntries, ...skippedJsonEntries], 'json'));
904
- return;
905
- }
906
- const totalEntries = entries.length + skippedDuplicates.length + skippedStdio.length;
907
- const fileCount = discovered.length + scan.empty.length + scan.errors.length;
908
- console.log(theme.cyan(`Found ${fileCount} MCP config file${fileCount === 1 ? '' : 's'} ` +
909
- `with ${totalEntries} server${totalEntries === 1 ? '' : 's'}:`));
910
- const statusByName = new Map(results.map((r) => [r.sessionName, r]));
911
- const liveSkippedStdio = new Set();
912
- for (const s of skippedStdio) {
913
- const session = await getSession(s.sessionName);
914
- if (session && getBridgeStatus(session) === 'live') {
915
- liveSkippedStdio.add(s.sessionName);
916
- }
917
- }
918
- const unconnectedStdio = skippedStdio.length - liveSkippedStdio.size;
919
- for (const d of discovered) {
920
- console.log(` ${formatPath(d.path)} ${chalk.dim(`(${d.serverCount} server${d.serverCount === 1 ? '' : 's'})`)}`);
921
- for (const entryName of Object.keys(d.config.mcpServers)) {
922
- const sessionName = generateSessionName({ type: 'config', file: d.path, entry: entryName });
923
- const serverCfg = d.config.mcpServers[entryName];
924
- const target = serverCfg?.url ?? [serverCfg?.command, ...(serverCfg?.args ?? [])].join(' ');
925
- const truncated = target && target.length > 72 ? target.slice(0, 72) + '…' : target;
926
- let marker;
927
- if (skippedStdio.some((s) => s.configFile === d.path && s.entry === entryName)) {
928
- marker = liveSkippedStdio.has(sessionName)
929
- ? formatConnectStatusBadge('active')
930
- : theme.yellow('○ skipped (stdio)');
931
- }
932
- else if (skippedDuplicates.some((s) => s.configFile === d.path && s.entry === entryName)) {
933
- marker = chalk.dim('○ skipped (duplicate)');
934
- }
935
- else {
936
- const r = statusByName.get(sessionName);
937
- marker = r ? formatConnectStatusBadge(r.status, r.error) : '';
938
- }
939
- console.log(` ${theme.cyan(sessionName)} → ${chalk.dim(truncated ?? entryName)}${marker ? ` ${marker}` : ''}`);
940
- }
941
- }
942
- for (const c of scan.empty) {
943
- console.log(` ${formatPath(c.path)} ${chalk.dim('(0 servers)')}`);
944
- }
945
- for (const c of scan.errors) {
946
- console.log(` ${formatPath(c.path)} ${chalk.dim('(invalid)')}`);
947
- console.log(` ${chalk.dim(c.error)}`);
948
- }
949
- if (entries.length === 0 && !hasApifyToken && liveSkippedStdio.size === 0) {
950
- throw new ClientError(`All servers in discovered config files use stdio transport.\n` +
951
- `Pass --stdio to include them: mcpc connect --stdio`);
952
- }
953
- if (unconnectedStdio > 0) {
954
- console.log('\nTo include stdio servers, run: mcpc connect --stdio');
955
- }
956
- await maybeConnectApify(entries, results, options);
957
- const failed = results.filter((r) => r.status === 'failed').length;
958
- const succeeded = results.filter((r) => r.status === 'active' || r.status === 'created').length;
959
- if (entries.length > 0 && succeeded === 0 && failed > 0) {
960
- throw new ClientError(`Failed to connect any servers from discovered config files`);
961
- }
962
- }
963
- const APIFY_MCP_URL = 'https://mcp.apify.com';
964
- const APIFY_SESSION_NAME = '@apify';
965
- async function maybeConnectApify(configEntries, configResults, options) {
966
- const token = process.env.APIFY_API_TOKEN;
967
- if (!token)
968
- return;
969
- if (configEntries.some((e) => e.sessionName === APIFY_SESSION_NAME))
970
- return;
971
- if (configResults.some((r) => r.sessionName === APIFY_SESSION_NAME))
972
- return;
973
- const existing = await getSession(APIFY_SESSION_NAME);
974
- const isLive = existing && getBridgeStatus(existing) === 'live';
975
- if (options.outputMode === 'human') {
976
- console.log(theme.cyan(`\nAPIFY_API_TOKEN detected, connecting to ${APIFY_MCP_URL}...`));
977
- }
978
- if (isLive) {
979
- if (options.outputMode === 'human') {
980
- console.log(` ${theme.green('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${theme.green('live')}`);
981
- }
982
- return;
983
- }
984
- try {
985
- await connectSession(APIFY_MCP_URL, APIFY_SESSION_NAME, {
986
- outputMode: options.outputMode,
987
- ...(options.verbose && { verbose: true }),
988
- headers: [`Authorization: Bearer ${token}`],
989
- skipDetails: true,
990
- quiet: true,
991
- noProfile: true,
992
- });
993
- if (options.outputMode === 'human') {
994
- console.log(` ${theme.yellow('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${theme.yellow('connecting')}`);
995
- }
996
- }
997
- catch (error) {
998
- const msg = error instanceof Error ? error.message : String(error);
999
- if (options.outputMode === 'human') {
1000
- console.log(` ${theme.red('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${theme.red('failed')}${chalk.dim(` — ${msg}`)}`);
1001
- }
1002
- }
1003
- }
1004
295
  export async function openShell(target) {
1005
296
  console.error(formatWarning('The "shell" command is deprecated and will be removed in a future release.'));
1006
297
  const { startShell } = await import('../shell.js');