@apify/mcpc 0.3.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.
Files changed (111) hide show
  1. package/CHANGELOG.md +37 -2
  2. package/README.md +171 -48
  3. package/dist/bridge/index.js +32 -40
  4. package/dist/bridge/index.js.map +1 -1
  5. package/dist/cli/commands/clean.d.ts.map +1 -1
  6. package/dist/cli/commands/clean.js +3 -2
  7. package/dist/cli/commands/clean.js.map +1 -1
  8. package/dist/cli/commands/connect.d.ts +63 -0
  9. package/dist/cli/commands/connect.d.ts.map +1 -0
  10. package/dist/cli/commands/connect.js +655 -0
  11. package/dist/cli/commands/connect.js.map +1 -0
  12. package/dist/cli/commands/logs.d.ts +8 -0
  13. package/dist/cli/commands/logs.d.ts.map +1 -0
  14. package/dist/cli/commands/logs.js +143 -0
  15. package/dist/cli/commands/logs.js.map +1 -0
  16. package/dist/cli/commands/sessions.d.ts +6 -59
  17. package/dist/cli/commands/sessions.d.ts.map +1 -1
  18. package/dist/cli/commands/sessions.js +37 -690
  19. package/dist/cli/commands/sessions.js.map +1 -1
  20. package/dist/cli/commands/skills.d.ts +20 -0
  21. package/dist/cli/commands/skills.d.ts.map +1 -0
  22. package/dist/cli/commands/skills.js +160 -0
  23. package/dist/cli/commands/skills.js.map +1 -0
  24. package/dist/cli/commands/x402.d.ts.map +1 -1
  25. package/dist/cli/commands/x402.js +14 -4
  26. package/dist/cli/commands/x402.js.map +1 -1
  27. package/dist/cli/index.js +117 -31
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cli/output.d.ts +13 -0
  30. package/dist/cli/output.d.ts.map +1 -1
  31. package/dist/cli/output.js +113 -20
  32. package/dist/cli/output.js.map +1 -1
  33. package/dist/cli/parser.d.ts +1 -1
  34. package/dist/cli/parser.d.ts.map +1 -1
  35. package/dist/cli/parser.js +25 -2
  36. package/dist/cli/parser.js.map +1 -1
  37. package/dist/cli/shell.js.map +1 -1
  38. package/dist/core/capabilities.d.ts +3 -0
  39. package/dist/core/capabilities.d.ts.map +1 -0
  40. package/dist/core/capabilities.js +14 -0
  41. package/dist/core/capabilities.js.map +1 -0
  42. package/dist/core/index.d.ts +1 -0
  43. package/dist/core/index.d.ts.map +1 -1
  44. package/dist/core/index.js +1 -0
  45. package/dist/core/index.js.map +1 -1
  46. package/dist/core/mcp-client.d.ts +4 -0
  47. package/dist/core/mcp-client.d.ts.map +1 -1
  48. package/dist/core/mcp-client.js +36 -12
  49. package/dist/core/mcp-client.js.map +1 -1
  50. package/dist/lib/bridge-client.d.ts +4 -1
  51. package/dist/lib/bridge-client.d.ts.map +1 -1
  52. package/dist/lib/bridge-client.js +29 -3
  53. package/dist/lib/bridge-client.js.map +1 -1
  54. package/dist/lib/bridge-manager.d.ts +3 -3
  55. package/dist/lib/bridge-manager.d.ts.map +1 -1
  56. package/dist/lib/bridge-manager.js +36 -20
  57. package/dist/lib/bridge-manager.js.map +1 -1
  58. package/dist/lib/cleanup.d.ts.map +1 -1
  59. package/dist/lib/cleanup.js +20 -19
  60. package/dist/lib/cleanup.js.map +1 -1
  61. package/dist/lib/config.d.ts +14 -0
  62. package/dist/lib/config.d.ts.map +1 -1
  63. package/dist/lib/config.js +41 -27
  64. package/dist/lib/config.js.map +1 -1
  65. package/dist/lib/errors.d.ts +0 -1
  66. package/dist/lib/errors.d.ts.map +1 -1
  67. package/dist/lib/errors.js +2 -4
  68. package/dist/lib/errors.js.map +1 -1
  69. package/dist/lib/index.d.ts +1 -0
  70. package/dist/lib/index.d.ts.map +1 -1
  71. package/dist/lib/index.js +1 -0
  72. package/dist/lib/index.js.map +1 -1
  73. package/dist/lib/log-reader.d.ts +28 -0
  74. package/dist/lib/log-reader.d.ts.map +1 -0
  75. package/dist/lib/log-reader.js +278 -0
  76. package/dist/lib/log-reader.js.map +1 -0
  77. package/dist/lib/session-client.d.ts +1 -1
  78. package/dist/lib/session-client.d.ts.map +1 -1
  79. package/dist/lib/session-client.js +6 -8
  80. package/dist/lib/session-client.js.map +1 -1
  81. package/dist/lib/stderr-tail.d.ts +10 -0
  82. package/dist/lib/stderr-tail.d.ts.map +1 -0
  83. package/dist/lib/stderr-tail.js +28 -0
  84. package/dist/lib/stderr-tail.js.map +1 -0
  85. package/dist/lib/types.d.ts +6 -1
  86. package/dist/lib/types.d.ts.map +1 -1
  87. package/dist/lib/types.js +1 -0
  88. package/dist/lib/types.js.map +1 -1
  89. package/dist/lib/utils.d.ts +1 -0
  90. package/dist/lib/utils.d.ts.map +1 -1
  91. package/dist/lib/utils.js +12 -2
  92. package/dist/lib/utils.js.map +1 -1
  93. package/dist/lib/x402/fetch-middleware.d.ts +3 -2
  94. package/dist/lib/x402/fetch-middleware.d.ts.map +1 -1
  95. package/dist/lib/x402/fetch-middleware.js +37 -24
  96. package/dist/lib/x402/fetch-middleware.js.map +1 -1
  97. package/dist/lib/x402/signer.d.ts +7 -1
  98. package/dist/lib/x402/signer.d.ts.map +1 -1
  99. package/dist/lib/x402/signer.js +249 -5
  100. package/dist/lib/x402/signer.js.map +1 -1
  101. package/docs/README.md +1 -0
  102. package/docs/claude-skill/SKILL.md +150 -117
  103. package/docs/examples/company-lookup.sh +6 -6
  104. package/docs/vhs/README.md +79 -0
  105. package/docs/vhs/grep.tape +56 -0
  106. package/docs/vhs/mcpc-demo.tape +179 -0
  107. package/docs/vhs/proxy.tape +69 -0
  108. package/docs/vhs/quickstart.tape +59 -0
  109. package/docs/vhs/scripting.tape +69 -0
  110. package/docs/vhs/tools.tape +66 -0
  111. package/package.json +20 -20
@@ -0,0 +1,655 @@
1
+ import { createServer } from 'net';
2
+ import { isValidSessionName, generateSessionName, normalizeServerUrl, validateProfileName, redactHeaders, AuthError, ClientError, isAuthenticationError, createServerAuthError, } from '../../lib/index.js';
3
+ import { formatOutput, formatSuccess, formatWarning, formatPath, formatConnectStatusBadge, theme, } from '../output.js';
4
+ import { withMcpClient, resolveTarget, resolveAuthProfile } from '../helpers.js';
5
+ import { deleteSession, saveSession, updateSession, getSession, loadSessions, } from '../../lib/sessions.js';
6
+ import { startBridge, stopBridge } from '../../lib/bridge-manager.js';
7
+ import { storeKeychainSessionHeaders, storeKeychainProxyBearerToken, } from '../../lib/auth/keychain.js';
8
+ import { getWallet } from '../../lib/wallets.js';
9
+ import chalk from 'chalk';
10
+ import ora from 'ora';
11
+ import { createLogger } from '../../lib/logger.js';
12
+ import { parseProxyArg } from '../parser.js';
13
+ import { loadConfig, listServers, isStdioEntry, scanMcpConfigFiles, getStandardMcpConfigPaths, } from '../../lib/config.js';
14
+ import { getBridgeStatus, showServerDetails, statelessField } from './sessions.js';
15
+ const logger = createLogger('connect');
16
+ async function checkPortAvailable(host, port) {
17
+ return new Promise((resolve) => {
18
+ const server = createServer();
19
+ server.once('error', () => {
20
+ resolve(false);
21
+ });
22
+ server.once('listening', () => {
23
+ server.close(() => {
24
+ resolve(true);
25
+ });
26
+ });
27
+ server.listen(port, host);
28
+ });
29
+ }
30
+ async function buildConnectResultEntry(sessionName, status, options) {
31
+ return await withMcpClient(sessionName, {
32
+ outputMode: 'json',
33
+ hideTarget: true,
34
+ ...(options.verbose && { verbose: options.verbose }),
35
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
36
+ }, async (client, context) => {
37
+ const serverDetails = await client.getServerDetails();
38
+ const tools = (await client.listAllTools()).tools;
39
+ const server = context.serverConfig
40
+ ? {
41
+ ...context.serverConfig,
42
+ ...(context.serverConfig.headers && {
43
+ headers: redactHeaders(context.serverConfig.headers),
44
+ }),
45
+ }
46
+ : undefined;
47
+ return {
48
+ _mcpc: {
49
+ sessionName: context.sessionName ?? sessionName,
50
+ ...(context.profileName && { profileName: context.profileName }),
51
+ ...(server && { server }),
52
+ ...(options.configFile && { configFile: options.configFile }),
53
+ ...(options.entry && { entry: options.entry }),
54
+ status,
55
+ ...statelessField(serverDetails.connectionMode),
56
+ },
57
+ ...(serverDetails.protocolVersion && { protocolVersion: serverDetails.protocolVersion }),
58
+ ...(serverDetails.capabilities && { capabilities: serverDetails.capabilities }),
59
+ ...(serverDetails.serverInfo && { serverInfo: serverDetails.serverInfo }),
60
+ ...(serverDetails.instructions && { instructions: serverDetails.instructions }),
61
+ ...(tools.length > 0 && { toolNames: tools.map((t) => t.name) }),
62
+ };
63
+ });
64
+ }
65
+ async function renderConnectedSession(name, status, options) {
66
+ if (options.outputMode === 'json') {
67
+ const entry = await buildConnectResultEntry(name, status, {
68
+ ...(options.verbose && { verbose: options.verbose }),
69
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
70
+ });
71
+ console.log(formatOutput([entry], 'json'));
72
+ }
73
+ else {
74
+ await showServerDetails(name, { ...options, hideTarget: false });
75
+ }
76
+ }
77
+ export async function connectSession(target, name, options) {
78
+ if (!isValidSessionName(name)) {
79
+ throw new ClientError(`Invalid session name: ${name}\n` +
80
+ `Session names must start with @ and be followed by 1-64 characters, alphanumeric with hyphens or underscores only (e.g., @my-session).`);
81
+ }
82
+ if (options.profile) {
83
+ validateProfileName(options.profile);
84
+ }
85
+ let proxyConfig;
86
+ if (options.proxy) {
87
+ proxyConfig = parseProxyArg(options.proxy);
88
+ logger.debug(`Proxy config: ${proxyConfig.host}:${proxyConfig.port}`);
89
+ const portAvailable = await checkPortAvailable(proxyConfig.host, proxyConfig.port);
90
+ if (!portAvailable) {
91
+ throw new ClientError(`Port ${proxyConfig.port} is already in use on ${proxyConfig.host}. ` +
92
+ `Choose a different port with --proxy [host:]port`);
93
+ }
94
+ }
95
+ if (options.proxyBearerToken && !options.proxy) {
96
+ throw new ClientError('--proxy-bearer-token requires --proxy to be specified');
97
+ }
98
+ const existingSession = await getSession(name);
99
+ if (existingSession) {
100
+ const bridgeStatus = getBridgeStatus(existingSession);
101
+ if (bridgeStatus === 'live') {
102
+ if (options.outputMode === 'human' && !options.quiet) {
103
+ console.log(formatSuccess(`Session ${name} is already active`));
104
+ }
105
+ if (!options.skipDetails) {
106
+ await renderConnectedSession(name, 'active', options);
107
+ }
108
+ return;
109
+ }
110
+ if (options.outputMode === 'human' && !options.quiet) {
111
+ console.log(theme.yellow(`Session ${name} exists but bridge is ${bridgeStatus}, reconnecting...`));
112
+ }
113
+ try {
114
+ await stopBridge(name);
115
+ }
116
+ catch {
117
+ }
118
+ }
119
+ const serverConfig = await resolveTarget(target, options);
120
+ const hasExplicitAuthHeader = serverConfig.headers?.Authorization !== undefined;
121
+ const hasExplicitProfile = options.profile !== undefined;
122
+ if (hasExplicitAuthHeader && hasExplicitProfile) {
123
+ throw new ClientError(`Cannot combine --profile with --header "Authorization: ...".\n\n` +
124
+ `Use either:\n` +
125
+ ` --profile ${options.profile} (OAuth authentication via saved profile)\n` +
126
+ ` --header "Authorization: Bearer <token>" (static bearer token)`);
127
+ }
128
+ let profileName;
129
+ if (serverConfig.url) {
130
+ if (options.noProfile) {
131
+ logger.debug('Skipping OAuth profile: --no-profile specified');
132
+ }
133
+ else if (hasExplicitAuthHeader) {
134
+ logger.debug('Skipping OAuth profile auto-detection: explicit Authorization header provided via --header');
135
+ }
136
+ else if (options.x402 && !options.profile) {
137
+ logger.debug('Skipping OAuth profile auto-detection: --x402 specified');
138
+ }
139
+ else {
140
+ profileName = await resolveAuthProfile(serverConfig.url, target, options.profile, {
141
+ sessionName: name,
142
+ });
143
+ }
144
+ }
145
+ let headers;
146
+ if (serverConfig.headers && Object.keys(serverConfig.headers).length > 0) {
147
+ headers = { ...serverConfig.headers };
148
+ logger.debug(`Storing ${Object.keys(headers).length} headers for session ${name} in keychain`);
149
+ await storeKeychainSessionHeaders(name, headers);
150
+ }
151
+ if (options.proxyBearerToken) {
152
+ logger.debug(`Storing proxy bearer token for session ${name} in keychain`);
153
+ await storeKeychainProxyBearerToken(name, options.proxyBearerToken);
154
+ }
155
+ if (options.x402) {
156
+ const wallet = await getWallet();
157
+ if (!wallet) {
158
+ throw new ClientError('x402 wallet not found. Create one with: mcpc x402 init');
159
+ }
160
+ logger.debug(`Using x402 wallet: ${wallet.address}`);
161
+ }
162
+ const isReconnect = !!existingSession;
163
+ const { headers: _originalHeaders, ...baseTransportConfig } = serverConfig;
164
+ const sessionTransportConfig = {
165
+ ...baseTransportConfig,
166
+ ...(headers && { headers: redactHeaders(headers) }),
167
+ };
168
+ const sessionUpdate = {
169
+ server: sessionTransportConfig,
170
+ ...(profileName && { profileName }),
171
+ ...(proxyConfig && { proxy: proxyConfig }),
172
+ ...(options.x402 && { x402: options.x402 }),
173
+ ...(options.insecure && { insecure: true }),
174
+ ...(isReconnect && { status: 'active' }),
175
+ };
176
+ if (isReconnect) {
177
+ await updateSession(name, sessionUpdate);
178
+ logger.debug(`Session record updated for reconnect: ${name}`);
179
+ }
180
+ else {
181
+ await saveSession(name, {
182
+ server: sessionTransportConfig,
183
+ createdAt: new Date().toISOString(),
184
+ status: 'connecting',
185
+ lastConnectionAttemptAt: new Date().toISOString(),
186
+ ...sessionUpdate,
187
+ });
188
+ logger.debug(`Initial session record created for: ${name}`);
189
+ }
190
+ try {
191
+ const bridgeOptions = {
192
+ sessionName: name,
193
+ serverConfig,
194
+ verbose: options.verbose || false,
195
+ ...(headers && { headers }),
196
+ ...(profileName && { profileName }),
197
+ ...(proxyConfig && { proxyConfig }),
198
+ ...(options.x402 && { x402: options.x402 }),
199
+ ...(options.insecure && { insecure: true }),
200
+ };
201
+ const { pid } = await startBridge(bridgeOptions);
202
+ await updateSession(name, { pid, status: 'active' });
203
+ logger.debug(`Session ${name} updated with bridge PID: ${pid}`);
204
+ }
205
+ catch (error) {
206
+ logger.debug(`Bridge start failed, cleaning up session ${name}`);
207
+ if (!isReconnect) {
208
+ try {
209
+ await deleteSession(name);
210
+ }
211
+ catch {
212
+ }
213
+ }
214
+ throw error;
215
+ }
216
+ if (options.skipDetails) {
217
+ if (options.outputMode === 'human' && !options.quiet) {
218
+ console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
219
+ }
220
+ return;
221
+ }
222
+ try {
223
+ await renderConnectedSession(name, 'created', options);
224
+ if (options.outputMode === 'human') {
225
+ console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
226
+ }
227
+ }
228
+ catch (detailsError) {
229
+ if (detailsError instanceof AuthError) {
230
+ throw detailsError;
231
+ }
232
+ if (detailsError instanceof Error && isAuthenticationError(detailsError.message)) {
233
+ throw createServerAuthError(serverConfig.url || target, { sessionName: name });
234
+ }
235
+ const errorMsg = detailsError instanceof Error ? detailsError.message : String(detailsError);
236
+ if (options.outputMode === 'json') {
237
+ const failed = {
238
+ _mcpc: {
239
+ sessionName: name,
240
+ status: 'failed',
241
+ error: errorMsg,
242
+ },
243
+ };
244
+ console.log(formatOutput([failed], 'json'));
245
+ }
246
+ else {
247
+ console.log(formatWarning(`Session ${name} created but server is not responding: ${errorMsg}\n` +
248
+ ` The session will auto-recover when the server becomes available.\n` +
249
+ ` Check status with: mcpc ${name}`));
250
+ }
251
+ logger.debug(`showServerDetails failed for new session ${name}: ${errorMsg}`);
252
+ }
253
+ }
254
+ async function findMatchingSession(parsed, options) {
255
+ const storage = await loadSessions();
256
+ const sessions = Object.values(storage.sessions);
257
+ if (sessions.length === 0)
258
+ return undefined;
259
+ const effectiveProfile = options.noProfile ? undefined : (options.profile ?? 'default');
260
+ for (const session of sessions) {
261
+ if (!session.server)
262
+ continue;
263
+ if (parsed.type === 'url') {
264
+ if (!session.server.url)
265
+ continue;
266
+ try {
267
+ const existingUrl = normalizeServerUrl(session.server.url);
268
+ const newUrl = normalizeServerUrl(parsed.url);
269
+ if (existingUrl !== newUrl)
270
+ continue;
271
+ }
272
+ catch {
273
+ continue;
274
+ }
275
+ }
276
+ else {
277
+ continue;
278
+ }
279
+ const sessionProfile = session.profileName ?? 'default';
280
+ if (effectiveProfile !== sessionProfile)
281
+ continue;
282
+ const existingHeaderKeys = Object.keys(session.server.headers || {}).sort();
283
+ const newHeaderKeys = (options.headers || [])
284
+ .map((h) => h.split(':')[0]?.trim() || '')
285
+ .filter(Boolean)
286
+ .sort();
287
+ if (existingHeaderKeys.join(',') !== newHeaderKeys.join(','))
288
+ continue;
289
+ return session.name;
290
+ }
291
+ return undefined;
292
+ }
293
+ export async function resolveSessionName(parsed, options) {
294
+ const existingName = await findMatchingSession(parsed, options);
295
+ if (existingName) {
296
+ return existingName;
297
+ }
298
+ const candidateName = generateSessionName(parsed);
299
+ const storage = await loadSessions();
300
+ if (!(candidateName in storage.sessions)) {
301
+ if (options.outputMode === 'human') {
302
+ console.log(theme.cyan(`Using session name: ${candidateName}`));
303
+ }
304
+ return candidateName;
305
+ }
306
+ for (let i = 2; i <= 99; i++) {
307
+ const suffixed = `${candidateName}-${i}`;
308
+ if (isValidSessionName(suffixed) && !(suffixed in storage.sessions)) {
309
+ if (options.outputMode === 'human') {
310
+ console.log(theme.cyan(`Using session name: ${suffixed}`));
311
+ }
312
+ return suffixed;
313
+ }
314
+ }
315
+ throw new ClientError(`Cannot auto-generate session name: too many sessions for this server.\n` +
316
+ `Specify a name explicitly: mcpc connect ${parsed.type === 'url' ? parsed.url : `${parsed.file}:${parsed.entry}`} @my-session`);
317
+ }
318
+ function skippedConnectEntry(entry, skipReason) {
319
+ return {
320
+ _mcpc: {
321
+ sessionName: entry.sessionName,
322
+ configFile: entry.configFile,
323
+ entry: entry.entry,
324
+ status: 'skipped',
325
+ skipReason,
326
+ },
327
+ };
328
+ }
329
+ async function waitForSessionReady(sessionName, options) {
330
+ try {
331
+ await withMcpClient(sessionName, {
332
+ outputMode: 'json',
333
+ hideTarget: true,
334
+ ...(options.verbose && { verbose: options.verbose }),
335
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
336
+ }, async (client) => {
337
+ await client.getServerDetails();
338
+ });
339
+ return { ready: true };
340
+ }
341
+ catch (error) {
342
+ return { ready: false, error: error instanceof Error ? error.message : String(error) };
343
+ }
344
+ }
345
+ async function waitForBulkConnectReady(results, options, onSettled) {
346
+ return Promise.all(results.map(async (r) => {
347
+ try {
348
+ if (r.status !== 'created')
349
+ return r;
350
+ const ready = await waitForSessionReady(r.sessionName, options);
351
+ return ready.ready ? r : { ...r, status: 'failed', error: ready.error };
352
+ }
353
+ finally {
354
+ onSettled?.();
355
+ }
356
+ }));
357
+ }
358
+ async function bulkConnectEntries(entries, options, { printBadges = true } = {}) {
359
+ const liveSet = new Set();
360
+ for (const { sessionName } of entries) {
361
+ const session = await getSession(sessionName);
362
+ if (session && getBridgeStatus(session) === 'live') {
363
+ liveSet.add(sessionName);
364
+ }
365
+ }
366
+ const settled = await Promise.allSettled(entries.map(async ({ entry, sessionName, configFile }) => connectSession(entry, sessionName, {
367
+ ...options,
368
+ config: configFile,
369
+ skipDetails: true,
370
+ quiet: true,
371
+ })));
372
+ let results = settled.map((outcome, i) => {
373
+ const base = entries[i];
374
+ if (outcome.status === 'fulfilled') {
375
+ return { ...base, status: liveSet.has(base.sessionName) ? 'active' : 'created' };
376
+ }
377
+ const error = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
378
+ return { ...base, status: 'failed', error };
379
+ });
380
+ if (options.outputMode === 'human' && results.some((r) => r.status === 'created')) {
381
+ const total = results.length;
382
+ let done = 0;
383
+ const spinner = ora(`Connecting to ${total} server${total === 1 ? '' : 's'}...`).start();
384
+ results = await waitForBulkConnectReady(results, options, () => {
385
+ done += 1;
386
+ spinner.text = `Connecting to servers... (${done}/${total})`;
387
+ });
388
+ spinner.stop();
389
+ }
390
+ if (options.outputMode === 'human' && printBadges) {
391
+ for (const r of results) {
392
+ const name = theme.cyan(r.sessionName);
393
+ switch (r.status) {
394
+ case 'created':
395
+ console.log(` ${theme.green('●')} ${name} ${theme.green('live')}`);
396
+ break;
397
+ case 'active':
398
+ console.log(` ${theme.green('●')} ${name} ${chalk.dim('already active')}`);
399
+ break;
400
+ case 'failed':
401
+ console.log(` ${theme.red('●')} ${name} ${theme.red('failed')}${r.error ? chalk.dim(` — ${r.error}`) : ''}`);
402
+ break;
403
+ }
404
+ }
405
+ }
406
+ return results;
407
+ }
408
+ function printBulkConnectSummary(results, options) {
409
+ const active = results.filter((r) => r.status === 'active').length;
410
+ const connected = results.filter((r) => r.status === 'created').length;
411
+ const failed = results.filter((r) => r.status === 'failed').length;
412
+ if (options.outputMode === 'human' && results.length > 1) {
413
+ const parts = [];
414
+ if (connected > 0)
415
+ parts.push(`${connected} connected`);
416
+ if (active > 0)
417
+ parts.push(`${active} already active`);
418
+ if (failed > 0)
419
+ parts.push(`${failed} failed`);
420
+ const summary = parts.join(', ');
421
+ if (failed === 0) {
422
+ console.log(formatSuccess(summary));
423
+ }
424
+ else if (active + connected > 0) {
425
+ console.log(formatWarning(summary));
426
+ }
427
+ }
428
+ return { active, connected, failed };
429
+ }
430
+ async function buildBulkConnectEntries(results, options) {
431
+ return await Promise.all(results.map(async (r) => {
432
+ if (r.status === 'failed') {
433
+ return {
434
+ _mcpc: {
435
+ sessionName: r.sessionName,
436
+ configFile: r.configFile,
437
+ entry: r.entry,
438
+ status: 'failed',
439
+ ...(r.error && { error: r.error }),
440
+ },
441
+ };
442
+ }
443
+ try {
444
+ return await buildConnectResultEntry(r.sessionName, r.status, {
445
+ ...(options.verbose && { verbose: options.verbose }),
446
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
447
+ configFile: r.configFile,
448
+ entry: r.entry,
449
+ });
450
+ }
451
+ catch (err) {
452
+ return {
453
+ _mcpc: {
454
+ sessionName: r.sessionName,
455
+ configFile: r.configFile,
456
+ entry: r.entry,
457
+ status: 'failed',
458
+ error: err instanceof Error ? err.message : String(err),
459
+ },
460
+ };
461
+ }
462
+ }));
463
+ }
464
+ export async function connectAllFromConfig(configFile, options) {
465
+ const config = loadConfig(configFile);
466
+ const allNames = listServers(config);
467
+ if (allNames.length === 0) {
468
+ throw new ClientError(`No servers found in config file: ${configFile}`);
469
+ }
470
+ const stdioSkipped = [];
471
+ const serverNames = allNames.filter((name) => {
472
+ if (!options.stdio && isStdioEntry(config, name)) {
473
+ stdioSkipped.push(name);
474
+ return false;
475
+ }
476
+ return true;
477
+ });
478
+ const toSkippedStdioEntry = (entry) => skippedConnectEntry({
479
+ sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
480
+ configFile,
481
+ entry,
482
+ }, 'stdio');
483
+ if (serverNames.length === 0) {
484
+ if (options.outputMode === 'json') {
485
+ console.log(formatOutput(stdioSkipped.map(toSkippedStdioEntry), 'json'));
486
+ return;
487
+ }
488
+ throw new ClientError(`All ${allNames.length} server${allNames.length === 1 ? '' : 's'} in ${configFile} use stdio transport.\n` +
489
+ `Pass --stdio to include them: mcpc connect ${configFile} --stdio`);
490
+ }
491
+ if (options.outputMode === 'human') {
492
+ console.log(theme.cyan(`Connecting ${serverNames.length} server${serverNames.length === 1 ? '' : 's'} from ${configFile}...`));
493
+ if (stdioSkipped.length > 0) {
494
+ console.log(chalk.dim(` skipping ${stdioSkipped.length} stdio server${stdioSkipped.length === 1 ? '' : 's'} ` +
495
+ `(${stdioSkipped.join(', ')}), pass --stdio to include`));
496
+ }
497
+ }
498
+ const entries = serverNames.map((entry) => ({
499
+ configFile,
500
+ entry,
501
+ sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
502
+ }));
503
+ const results = await bulkConnectEntries(entries, options);
504
+ if (options.outputMode === 'json') {
505
+ const resultEntries = await buildBulkConnectEntries(results, options);
506
+ console.log(formatOutput([...resultEntries, ...stdioSkipped.map(toSkippedStdioEntry)], 'json'));
507
+ return;
508
+ }
509
+ const { active, connected, failed } = printBulkConnectSummary(results, options);
510
+ if (active + connected === 0 && failed > 0) {
511
+ throw new ClientError(`Failed to connect any servers from ${configFile}`);
512
+ }
513
+ }
514
+ function aggregateDiscoveredEntries(discovered, options) {
515
+ const entries = [];
516
+ const skippedDuplicates = [];
517
+ const skippedStdio = [];
518
+ const seenNames = new Set();
519
+ for (const d of discovered) {
520
+ for (const entry of Object.keys(d.config.mcpServers)) {
521
+ const sessionName = generateSessionName({ type: 'config', file: d.path, entry });
522
+ if (!options.stdio && isStdioEntry(d.config, entry)) {
523
+ skippedStdio.push({ configFile: d.path, entry, sessionName });
524
+ continue;
525
+ }
526
+ if (seenNames.has(sessionName)) {
527
+ skippedDuplicates.push({ configFile: d.path, entry, sessionName });
528
+ continue;
529
+ }
530
+ seenNames.add(sessionName);
531
+ entries.push({
532
+ configFile: d.path,
533
+ entry,
534
+ sessionName,
535
+ });
536
+ }
537
+ }
538
+ return { entries, skippedDuplicates, skippedStdio };
539
+ }
540
+ function buildNoServersError(scan) {
541
+ if (scan.empty.length > 0 || scan.errors.length > 0) {
542
+ const lines = [];
543
+ if (scan.empty.length > 0) {
544
+ lines.push(scan.empty.length === 1
545
+ ? `Found a config file, but it defines no servers:`
546
+ : `Found config files, but they define no servers:`);
547
+ for (const c of scan.empty)
548
+ lines.push(` ${formatPath(c.path)}`);
549
+ }
550
+ if (scan.errors.length > 0) {
551
+ if (lines.length > 0)
552
+ lines.push('');
553
+ lines.push(scan.errors.length === 1
554
+ ? `Found a config file, but it couldn't be used:`
555
+ : `Found config files, but they couldn't be used:`);
556
+ for (const c of scan.errors)
557
+ lines.push(` ${formatPath(c.path)} — ${c.error}`);
558
+ }
559
+ return (`No MCP servers to connect.\n\n` +
560
+ `${lines.join('\n')}\n\n` +
561
+ `Add a server under "mcpServers" and re-run mcpc connect, or connect one now:\n` +
562
+ ` mcpc connect mcp.example.com @myserver`);
563
+ }
564
+ const searchPaths = getStandardMcpConfigPaths()
565
+ .map((c) => ` ${formatPath(c.path)}`)
566
+ .join('\n');
567
+ return (`No MCP config files found in standard locations.\n\n` +
568
+ `Searched:\n${searchPaths}\n\n` +
569
+ `Connect a specific server: mcpc connect mcp.example.com\n` +
570
+ `Connect from a specific file: mcpc connect /path/to/mcp.json`);
571
+ }
572
+ export async function connectAllFromStandardConfigs(options) {
573
+ const scan = scanMcpConfigFiles();
574
+ const { discovered } = scan;
575
+ if (discovered.length === 0) {
576
+ if (options.outputMode === 'json') {
577
+ console.log(formatOutput([], 'json'));
578
+ return;
579
+ }
580
+ throw new ClientError(buildNoServersError(scan));
581
+ }
582
+ const { entries, skippedDuplicates, skippedStdio } = aggregateDiscoveredEntries(discovered, {
583
+ ...(options.stdio && { stdio: true }),
584
+ });
585
+ const results = entries.length > 0 ? await bulkConnectEntries(entries, options, { printBadges: false }) : [];
586
+ if (options.outputMode === 'json') {
587
+ const skippedJsonEntries = [
588
+ ...skippedStdio.map((s) => skippedConnectEntry(s, 'stdio')),
589
+ ...skippedDuplicates.map((s) => skippedConnectEntry(s, 'duplicate')),
590
+ ];
591
+ if (entries.length === 0) {
592
+ console.log(formatOutput(skippedJsonEntries, 'json'));
593
+ return;
594
+ }
595
+ const resultEntries = await buildBulkConnectEntries(results, options);
596
+ console.log(formatOutput([...resultEntries, ...skippedJsonEntries], 'json'));
597
+ return;
598
+ }
599
+ const totalEntries = entries.length + skippedDuplicates.length + skippedStdio.length;
600
+ const fileCount = discovered.length + scan.empty.length + scan.errors.length;
601
+ console.log(theme.cyan(`Found ${fileCount} MCP config file${fileCount === 1 ? '' : 's'} ` +
602
+ `with ${totalEntries} server${totalEntries === 1 ? '' : 's'}:`));
603
+ const statusByName = new Map(results.map((r) => [r.sessionName, r]));
604
+ const liveSkippedStdio = new Set();
605
+ for (const s of skippedStdio) {
606
+ const session = await getSession(s.sessionName);
607
+ if (session && getBridgeStatus(session) === 'live') {
608
+ liveSkippedStdio.add(s.sessionName);
609
+ }
610
+ }
611
+ const unconnectedStdio = skippedStdio.length - liveSkippedStdio.size;
612
+ for (const d of discovered) {
613
+ console.log(` ${formatPath(d.path)} ${chalk.dim(`(${d.serverCount} server${d.serverCount === 1 ? '' : 's'})`)}`);
614
+ for (const entryName of Object.keys(d.config.mcpServers)) {
615
+ const sessionName = generateSessionName({ type: 'config', file: d.path, entry: entryName });
616
+ const serverCfg = d.config.mcpServers[entryName];
617
+ const target = serverCfg?.url ?? [serverCfg?.command, ...(serverCfg?.args ?? [])].join(' ');
618
+ const truncated = target && target.length > 72 ? target.slice(0, 72) + '…' : target;
619
+ let marker;
620
+ if (skippedStdio.some((s) => s.configFile === d.path && s.entry === entryName)) {
621
+ marker = liveSkippedStdio.has(sessionName)
622
+ ? formatConnectStatusBadge('active')
623
+ : theme.yellow('○ skipped (stdio)');
624
+ }
625
+ else if (skippedDuplicates.some((s) => s.configFile === d.path && s.entry === entryName)) {
626
+ marker = chalk.dim('○ skipped (duplicate)');
627
+ }
628
+ else {
629
+ const r = statusByName.get(sessionName);
630
+ marker = r ? formatConnectStatusBadge(r.status, r.error) : '';
631
+ }
632
+ console.log(` ${theme.cyan(sessionName)} → ${chalk.dim(truncated ?? entryName)}${marker ? ` ${marker}` : ''}`);
633
+ }
634
+ }
635
+ for (const c of scan.empty) {
636
+ console.log(` ${formatPath(c.path)} ${chalk.dim('(0 servers)')}`);
637
+ }
638
+ for (const c of scan.errors) {
639
+ console.log(` ${formatPath(c.path)} ${chalk.dim('(invalid)')}`);
640
+ console.log(` ${chalk.dim(c.error)}`);
641
+ }
642
+ if (entries.length === 0 && liveSkippedStdio.size === 0) {
643
+ throw new ClientError(`All servers in discovered config files use stdio transport.\n` +
644
+ `Pass --stdio to include them: mcpc connect --stdio`);
645
+ }
646
+ if (unconnectedStdio > 0) {
647
+ console.log('\nTo include stdio servers, run: mcpc connect --stdio');
648
+ }
649
+ const failed = results.filter((r) => r.status === 'failed').length;
650
+ const succeeded = results.filter((r) => r.status === 'active' || r.status === 'created').length;
651
+ if (entries.length > 0 && succeeded === 0 && failed > 0) {
652
+ throw new ClientError(`Failed to connect any servers from discovered config files`);
653
+ }
654
+ }
655
+ //# sourceMappingURL=connect.js.map