@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
@@ -1,347 +1,20 @@
1
- import { createServer } from 'net';
2
- import { isValidSessionName, generateSessionName, normalizeServerUrl, validateProfileName, isProcessAlive, getServerHost, getLogsDir, redactHeaders, } from '../../lib/index.js';
1
+ import { isProcessAlive, getServerHost, redactHeaders, ClientError, } from '../../lib/index.js';
3
2
  import { DISCONNECTED_THRESHOLD_MS } from '../../lib/types.js';
4
3
  import { formatOutput, formatSuccess, formatWarning, formatError, formatSessionLine, formatServerDetails, theme, } from '../output.js';
5
- import { withMcpClient, resolveTarget, resolveAuthProfile } from '../helpers.js';
4
+ import { withMcpClient, resolveAuthProfile } from '../helpers.js';
6
5
  import { listAuthProfiles } from '../../lib/auth/profiles.js';
7
- import { sessionExists, deleteSession, saveSession, updateSession, consolidateSessions, getSession, loadSessions, } from '../../lib/sessions.js';
6
+ import { sessionExists, deleteSession, updateSession, consolidateSessions, getSession, } from '../../lib/sessions.js';
8
7
  import { startBridge, stopBridge, reconnectCrashedSessions, } from '../../lib/bridge-manager.js';
9
- import { storeKeychainSessionHeaders, storeKeychainProxyBearerToken, } from '../../lib/auth/keychain.js';
10
- import { AuthError, ClientError, isAuthenticationError, createServerAuthError, } from '../../lib/index.js';
11
- import { getWallet } from '../../lib/wallets.js';
12
8
  import chalk from 'chalk';
13
9
  import { createLogger } from '../../lib/logger.js';
14
- import { parseProxyArg } from '../parser.js';
15
- import { loadConfig, listServers, isStdioEntry, discoverMcpConfigFiles, getStandardMcpConfigPaths, } from '../../lib/config.js';
10
+ import { getBridgeLogPath } from '../../lib/log-reader.js';
16
11
  const logger = createLogger('sessions');
17
- async function checkPortAvailable(host, port) {
18
- return new Promise((resolve) => {
19
- const server = createServer();
20
- server.once('error', (err) => {
21
- if (err.code === 'EADDRINUSE') {
22
- resolve(false);
23
- }
24
- else {
25
- resolve(false);
26
- }
27
- });
28
- server.once('listening', () => {
29
- server.close(() => {
30
- resolve(true);
31
- });
32
- });
33
- server.listen(port, host);
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
- }
134
- export async function connectSession(target, name, options) {
135
- if (!isValidSessionName(name)) {
136
- throw new ClientError(`Invalid session name: ${name}\n` +
137
- `Session names must start with @ and be followed by 1-64 characters, alphanumeric with hyphens or underscores only (e.g., @my-session).`);
138
- }
139
- if (options.profile) {
140
- validateProfileName(options.profile);
141
- }
142
- let proxyConfig;
143
- if (options.proxy) {
144
- proxyConfig = parseProxyArg(options.proxy);
145
- logger.debug(`Proxy config: ${proxyConfig.host}:${proxyConfig.port}`);
146
- const portAvailable = await checkPortAvailable(proxyConfig.host, proxyConfig.port);
147
- if (!portAvailable) {
148
- throw new ClientError(`Port ${proxyConfig.port} is already in use on ${proxyConfig.host}. ` +
149
- `Choose a different port with --proxy [host:]port`);
150
- }
151
- }
152
- if (options.proxyBearerToken && !options.proxy) {
153
- throw new ClientError('--proxy-bearer-token requires --proxy to be specified');
154
- }
155
- const existingSession = await getSession(name);
156
- if (existingSession) {
157
- const bridgeStatus = getBridgeStatus(existingSession);
158
- if (bridgeStatus === 'live') {
159
- if (options.outputMode === 'human' && !options.quiet) {
160
- console.log(formatSuccess(`Session ${name} is already active`));
161
- }
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
- }
174
- return;
175
- }
176
- if (options.outputMode === 'human' && !options.quiet) {
177
- console.log(theme.yellow(`Session ${name} exists but bridge is ${bridgeStatus}, reconnecting...`));
178
- }
179
- try {
180
- await stopBridge(name);
181
- }
182
- catch {
183
- }
184
- }
185
- const serverConfig = await resolveTarget(target, options);
186
- const hasExplicitAuthHeader = serverConfig.headers?.Authorization !== undefined;
187
- const hasExplicitProfile = options.profile !== undefined;
188
- if (hasExplicitAuthHeader && hasExplicitProfile) {
189
- throw new ClientError(`Cannot combine --profile with --header "Authorization: ...".\n\n` +
190
- `Use either:\n` +
191
- ` --profile ${options.profile} (OAuth authentication via saved profile)\n` +
192
- ` --header "Authorization: Bearer <token>" (static bearer token)`);
193
- }
194
- let profileName;
195
- if (serverConfig.url) {
196
- if (options.noProfile) {
197
- logger.debug('Skipping OAuth profile: --no-profile specified');
198
- }
199
- else if (hasExplicitAuthHeader) {
200
- logger.debug('Skipping OAuth profile auto-detection: explicit Authorization header provided via --header');
201
- }
202
- else if (options.x402 && !options.profile) {
203
- logger.debug('Skipping OAuth profile auto-detection: --x402 specified');
204
- }
205
- else {
206
- profileName = await resolveAuthProfile(serverConfig.url, target, options.profile, {
207
- sessionName: name,
208
- });
209
- }
210
- }
211
- let headers;
212
- if (Object.keys(serverConfig.headers || {}).length > 0) {
213
- headers = { ...serverConfig.headers };
214
- if (Object.keys(headers).length > 0) {
215
- logger.debug(`Storing ${Object.keys(headers).length} headers for session ${name} in keychain`);
216
- await storeKeychainSessionHeaders(name, headers);
217
- }
218
- else {
219
- headers = undefined;
220
- }
221
- }
222
- if (options.proxyBearerToken) {
223
- logger.debug(`Storing proxy bearer token for session ${name} in keychain`);
224
- await storeKeychainProxyBearerToken(name, options.proxyBearerToken);
225
- }
226
- if (options.x402) {
227
- const wallet = await getWallet();
228
- if (!wallet) {
229
- throw new ClientError('x402 wallet not found. Create one with: mcpc x402 init');
230
- }
231
- logger.debug(`Using x402 wallet: ${wallet.address}`);
232
- }
233
- const isReconnect = !!existingSession;
234
- const { headers: _originalHeaders, ...baseTransportConfig } = serverConfig;
235
- const sessionTransportConfig = {
236
- ...baseTransportConfig,
237
- ...(headers && { headers: redactHeaders(headers) }),
238
- };
239
- const sessionUpdate = {
240
- server: sessionTransportConfig,
241
- ...(profileName && { profileName }),
242
- ...(proxyConfig && { proxy: proxyConfig }),
243
- ...(options.x402 && { x402: true }),
244
- ...(options.insecure && { insecure: true }),
245
- ...(isReconnect && { status: 'active' }),
246
- };
247
- if (isReconnect) {
248
- await updateSession(name, sessionUpdate);
249
- logger.debug(`Session record updated for reconnect: ${name}`);
250
- }
251
- else {
252
- await saveSession(name, {
253
- server: sessionTransportConfig,
254
- createdAt: new Date().toISOString(),
255
- status: 'connecting',
256
- lastConnectionAttemptAt: new Date().toISOString(),
257
- ...sessionUpdate,
258
- });
259
- logger.debug(`Initial session record created for: ${name}`);
260
- }
261
- try {
262
- const bridgeOptions = {
263
- sessionName: name,
264
- serverConfig: serverConfig,
265
- verbose: options.verbose || false,
266
- };
267
- if (headers) {
268
- bridgeOptions.headers = headers;
269
- }
270
- if (profileName) {
271
- bridgeOptions.profileName = profileName;
272
- }
273
- if (proxyConfig) {
274
- bridgeOptions.proxyConfig = proxyConfig;
275
- }
276
- if (options.x402) {
277
- bridgeOptions.x402 = true;
278
- }
279
- if (options.insecure) {
280
- bridgeOptions.insecure = true;
281
- }
282
- const { pid } = await startBridge(bridgeOptions);
283
- await updateSession(name, { pid, status: 'active' });
284
- logger.debug(`Session ${name} updated with bridge PID: ${pid}`);
285
- }
286
- catch (error) {
287
- logger.debug(`Bridge start failed, cleaning up session ${name}`);
288
- if (!isReconnect) {
289
- try {
290
- await deleteSession(name);
291
- }
292
- catch {
293
- }
294
- }
295
- throw error;
296
- }
297
- if (options.skipDetails) {
298
- if (options.outputMode === 'human' && !options.quiet) {
299
- console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
300
- }
301
- return;
302
- }
303
- try {
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
- }
318
- }
319
- catch (detailsError) {
320
- if (detailsError instanceof AuthError) {
321
- throw detailsError;
322
- }
323
- if (detailsError instanceof Error && isAuthenticationError(detailsError.message)) {
324
- const logPath = `${getLogsDir()}/bridge-${name}.log`;
325
- throw createServerAuthError(serverConfig.url || target, { sessionName: name, logPath });
326
- }
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}`);
344
- }
12
+ export function statelessField(connectionMode) {
13
+ if (connectionMode === 'stateless')
14
+ return { stateless: true };
15
+ if (connectionMode === 'stateful')
16
+ return { stateless: false };
17
+ return { stateless: null };
345
18
  }
346
19
  export function getBridgeStatus(session) {
347
20
  if (session.status === 'unauthorized') {
@@ -402,9 +75,12 @@ export function formatTimeAgo(isoDate) {
402
75
  return 'yesterday';
403
76
  if (diffDays < 7)
404
77
  return `${diffDays} days ago`;
405
- if (diffDays < 30)
406
- return `${Math.floor(diffDays / 7)} weeks ago`;
407
- return `${Math.floor(diffDays / 30)} months ago`;
78
+ if (diffDays < 30) {
79
+ const weeks = Math.floor(diffDays / 7);
80
+ return `${weeks} ${weeks === 1 ? 'week' : 'weeks'} ago`;
81
+ }
82
+ const months = Math.floor(diffDays / 30);
83
+ return `${months} ${months === 1 ? 'month' : 'months'} ago`;
408
84
  }
409
85
  export async function listSessionsAndAuthProfiles(options) {
410
86
  const consolidateResult = await consolidateSessions(false);
@@ -412,9 +88,10 @@ export async function listSessionsAndAuthProfiles(options) {
412
88
  reconnectCrashedSessions(consolidateResult.sessionsToRestart);
413
89
  const profiles = await listAuthProfiles();
414
90
  if (options.outputMode === 'json') {
415
- const sessionsWithStatus = sessions.map((session) => ({
91
+ const sessionsWithStatus = sessions.map(({ connectionMode, ...session }) => ({
416
92
  ...session,
417
93
  status: getBridgeStatus(session),
94
+ ...statelessField(connectionMode),
418
95
  }));
419
96
  console.log(formatOutput({
420
97
  sessions: sessionsWithStatus,
@@ -478,6 +155,7 @@ export async function listSessionsAndAuthProfiles(options) {
478
155
  }
479
156
  }
480
157
  }
158
+ return { hasSessions: sessions.length > 0 };
481
159
  }
482
160
  export async function closeSession(name, options) {
483
161
  try {
@@ -513,7 +191,7 @@ export async function closeSession(name, options) {
513
191
  export async function showServerDetails(target, options) {
514
192
  await withMcpClient(target, options, async (client, context) => {
515
193
  const serverDetails = await client.getServerDetails();
516
- const { serverInfo, capabilities, instructions, protocolVersion } = serverDetails;
194
+ const { serverInfo, capabilities, instructions, protocolVersion, connectionMode } = serverDetails;
517
195
  const cachedToolsResult = await client.listAllTools();
518
196
  const tools = cachedToolsResult.tools;
519
197
  if (options.outputMode === 'human') {
@@ -526,11 +204,17 @@ export async function showServerDetails(target, options) {
526
204
  headers: redactHeaders(context.serverConfig.headers),
527
205
  }),
528
206
  };
207
+ let logPath;
208
+ if (target.startsWith('@')) {
209
+ logPath = getBridgeLogPath(target);
210
+ }
529
211
  console.log(formatOutput({
530
212
  _mcpc: {
531
213
  sessionName: context.sessionName,
532
214
  profileName: context.profileName,
533
215
  server,
216
+ ...statelessField(connectionMode),
217
+ ...(logPath && { logPath }),
534
218
  },
535
219
  protocolVersion,
536
220
  capabilities,
@@ -561,14 +245,6 @@ export async function restartSession(name, options) {
561
245
  }
562
246
  const { readKeychainSessionHeaders } = await import('../../lib/auth/keychain.js');
563
247
  const headers = await readKeychainSessionHeaders(name);
564
- const bridgeOptions = {
565
- sessionName: name,
566
- serverConfig: { ...serverConfig, ...(headers && { headers }) },
567
- verbose: options.verbose || false,
568
- };
569
- if (headers) {
570
- bridgeOptions.headers = headers;
571
- }
572
248
  const hasExplicitAuthHeader = headers?.Authorization !== undefined;
573
249
  let profileName = session.profileName;
574
250
  if (!profileName && serverConfig.url && !hasExplicitAuthHeader && !session.x402) {
@@ -580,18 +256,16 @@ export async function restartSession(name, options) {
580
256
  await updateSession(name, { profileName });
581
257
  }
582
258
  }
583
- if (profileName) {
584
- bridgeOptions.profileName = profileName;
585
- }
586
- if (session.proxy) {
587
- bridgeOptions.proxyConfig = session.proxy;
588
- }
589
- if (session.x402) {
590
- bridgeOptions.x402 = session.x402;
591
- }
592
- if (session.insecure) {
593
- bridgeOptions.insecure = session.insecure;
594
- }
259
+ const bridgeOptions = {
260
+ sessionName: name,
261
+ serverConfig: { ...serverConfig, ...(headers && { headers }) },
262
+ verbose: options.verbose || false,
263
+ ...(headers && { headers }),
264
+ ...(profileName && { profileName }),
265
+ ...(session.proxy && { proxyConfig: session.proxy }),
266
+ ...(session.x402 && { x402: session.x402 }),
267
+ ...(session.insecure && { insecure: session.insecure }),
268
+ };
595
269
  const { pid } = await startBridge(bridgeOptions);
596
270
  await updateSession(name, { pid, status: 'active' });
597
271
  logger.debug(`Session ${name} restarted with bridge PID: ${pid}`);
@@ -618,335 +292,8 @@ export async function restartSession(name, options) {
618
292
  throw error;
619
293
  }
620
294
  }
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
- }
948
- }
949
295
  export async function openShell(target) {
296
+ console.error(formatWarning('The "shell" command is deprecated and will be removed in a future release.'));
950
297
  const { startShell } = await import('../shell.js');
951
298
  await startShell(target);
952
299
  }