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