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