@apify/mcpc 0.1.11-beta.3 → 0.1.11-beta.4

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 (86) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/README.md +184 -189
  3. package/dist/bridge/index.js +190 -13
  4. package/dist/bridge/index.js.map +1 -1
  5. package/dist/cli/commands/auth.d.ts +2 -0
  6. package/dist/cli/commands/auth.d.ts.map +1 -1
  7. package/dist/cli/commands/auth.js +11 -1
  8. package/dist/cli/commands/auth.js.map +1 -1
  9. package/dist/cli/commands/logging.d.ts.map +1 -1
  10. package/dist/cli/commands/logging.js +1 -4
  11. package/dist/cli/commands/logging.js.map +1 -1
  12. package/dist/cli/commands/sessions.d.ts +3 -1
  13. package/dist/cli/commands/sessions.d.ts.map +1 -1
  14. package/dist/cli/commands/sessions.js +47 -8
  15. package/dist/cli/commands/sessions.js.map +1 -1
  16. package/dist/cli/commands/tasks.d.ts +5 -0
  17. package/dist/cli/commands/tasks.d.ts.map +1 -0
  18. package/dist/cli/commands/tasks.js +53 -0
  19. package/dist/cli/commands/tasks.js.map +1 -0
  20. package/dist/cli/commands/tools.d.ts +2 -0
  21. package/dist/cli/commands/tools.d.ts.map +1 -1
  22. package/dist/cli/commands/tools.js +88 -2
  23. package/dist/cli/commands/tools.js.map +1 -1
  24. package/dist/cli/helpers.d.ts +1 -5
  25. package/dist/cli/helpers.d.ts.map +1 -1
  26. package/dist/cli/helpers.js +25 -169
  27. package/dist/cli/helpers.js.map +1 -1
  28. package/dist/cli/index.js +426 -200
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/output.d.ts +3 -1
  31. package/dist/cli/output.d.ts.map +1 -1
  32. package/dist/cli/output.js +71 -3
  33. package/dist/cli/output.js.map +1 -1
  34. package/dist/cli/parser.d.ts +11 -8
  35. package/dist/cli/parser.d.ts.map +1 -1
  36. package/dist/cli/parser.js +91 -65
  37. package/dist/cli/parser.js.map +1 -1
  38. package/dist/cli/shell.d.ts.map +1 -1
  39. package/dist/cli/shell.js +30 -3
  40. package/dist/cli/shell.js.map +1 -1
  41. package/dist/core/mcp-client.d.ts +10 -2
  42. package/dist/core/mcp-client.d.ts.map +1 -1
  43. package/dist/core/mcp-client.js +165 -0
  44. package/dist/core/mcp-client.js.map +1 -1
  45. package/dist/lib/auth/keychain.d.ts.map +1 -1
  46. package/dist/lib/auth/keychain.js +15 -8
  47. package/dist/lib/auth/keychain.js.map +1 -1
  48. package/dist/lib/auth/oauth-flow.d.ts +4 -1
  49. package/dist/lib/auth/oauth-flow.d.ts.map +1 -1
  50. package/dist/lib/auth/oauth-flow.js +21 -6
  51. package/dist/lib/auth/oauth-flow.js.map +1 -1
  52. package/dist/lib/auth/oauth-provider.d.ts +5 -0
  53. package/dist/lib/auth/oauth-provider.d.ts.map +1 -1
  54. package/dist/lib/auth/oauth-provider.js +16 -1
  55. package/dist/lib/auth/oauth-provider.js.map +1 -1
  56. package/dist/lib/bridge-client.d.ts +1 -1
  57. package/dist/lib/bridge-client.d.ts.map +1 -1
  58. package/dist/lib/bridge-client.js +17 -3
  59. package/dist/lib/bridge-client.js.map +1 -1
  60. package/dist/lib/bridge-manager.d.ts +1 -0
  61. package/dist/lib/bridge-manager.d.ts.map +1 -1
  62. package/dist/lib/bridge-manager.js +39 -17
  63. package/dist/lib/bridge-manager.js.map +1 -1
  64. package/dist/lib/errors.js +2 -2
  65. package/dist/lib/errors.js.map +1 -1
  66. package/dist/lib/file-lock.d.ts.map +1 -1
  67. package/dist/lib/file-lock.js +4 -2
  68. package/dist/lib/file-lock.js.map +1 -1
  69. package/dist/lib/session-client.d.ts +12 -2
  70. package/dist/lib/session-client.d.ts.map +1 -1
  71. package/dist/lib/session-client.js +113 -14
  72. package/dist/lib/session-client.js.map +1 -1
  73. package/dist/lib/sessions.d.ts.map +1 -1
  74. package/dist/lib/sessions.js +9 -4
  75. package/dist/lib/sessions.js.map +1 -1
  76. package/dist/lib/types.d.ts +30 -5
  77. package/dist/lib/types.d.ts.map +1 -1
  78. package/dist/lib/types.js +2 -0
  79. package/dist/lib/types.js.map +1 -1
  80. package/dist/lib/utils.d.ts +0 -2
  81. package/dist/lib/utils.d.ts.map +1 -1
  82. package/dist/lib/utils.js +1 -19
  83. package/dist/lib/utils.js.map +1 -1
  84. package/docs/TODOs.md +89 -35
  85. package/package.json +2 -2
  86. package/renovate.json +2 -1
package/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
3
3
  import { Command } from 'commander';
4
4
  import { setVerbose, setJsonMode, closeFileLogger } from '../lib/index.js';
5
- import { isMcpError, formatHumanError, NetworkError } from '../lib/index.js';
5
+ import { isMcpError, formatHumanError, ClientError } from '../lib/index.js';
6
6
  import { formatJson, formatJsonError, rainbow } from './output.js';
7
7
  import * as tools from './commands/tools.js';
8
8
  import * as resources from './commands/resources.js';
@@ -11,12 +11,16 @@ import * as sessions from './commands/sessions.js';
11
11
  import * as logging from './commands/logging.js';
12
12
  import * as utilities from './commands/utilities.js';
13
13
  import * as auth from './commands/auth.js';
14
+ import * as tasks from './commands/tasks.js';
14
15
  import { handleX402Command } from './commands/x402.js';
15
16
  import { clean } from './commands/clean.js';
16
- import { findTarget, extractOptions, hasCommandAfterTarget, getVerboseFromEnv, getJsonFromEnv, validateOptions, validateCleanTypes, validateArgValues, KNOWN_COMMANDS, } from './parser.js';
17
+ import { extractOptions, getVerboseFromEnv, getJsonFromEnv, validateOptions, validateArgValues, parseServerArg, hasSubcommand, optionTakesValue, KNOWN_COMMANDS, KNOWN_SESSION_COMMANDS, } from './parser.js';
17
18
  import { createRequire } from 'module';
18
19
  const { version: mcpcVersion } = createRequire(import.meta.url)('../../package.json');
19
- setGlobalDispatcher(new EnvHttpProxyAgent());
20
+ {
21
+ const insecure = process.argv.includes('--insecure');
22
+ setGlobalDispatcher(new EnvHttpProxyAgent(insecure ? { connect: { rejectUnauthorized: false } } : {}));
23
+ }
20
24
  function getOptionsFromCommand(command) {
21
25
  const opts = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
22
26
  const verbose = opts.verbose || getVerboseFromEnv();
@@ -28,25 +32,31 @@ function getOptionsFromCommand(command) {
28
32
  const options = {
29
33
  outputMode: (json ? 'json' : 'human'),
30
34
  };
31
- if (opts.config)
32
- options.config = opts.config;
33
- if (opts.header) {
34
- options.headers = Array.isArray(opts.header) ? opts.header : [opts.header];
35
+ if (opts.timeout) {
36
+ const timeout = parseInt(opts.timeout, 10);
37
+ if (isNaN(timeout) || timeout <= 0) {
38
+ throw new Error(`Invalid --timeout value: "${opts.timeout}". Must be a positive number (seconds).`);
39
+ }
40
+ options.timeout = timeout;
41
+ }
42
+ if (opts.profile === false) {
43
+ options.noProfile = true;
35
44
  }
36
- if (opts.timeout)
37
- options.timeout = parseInt(opts.timeout, 10);
38
- if (opts.profile)
45
+ else if (opts.profile) {
39
46
  options.profile = opts.profile;
47
+ }
40
48
  if (verbose)
41
49
  options.verbose = verbose;
42
50
  if (opts.x402)
43
51
  options.x402 = true;
52
+ if (opts.insecure)
53
+ options.insecure = true;
44
54
  if (opts.schema)
45
55
  options.schema = opts.schema;
46
56
  if (opts.schemaMode) {
47
57
  const mode = opts.schemaMode;
48
58
  if (mode !== 'strict' && mode !== 'compatible' && mode !== 'ignore') {
49
- throw new Error(`Invalid schema mode: ${mode}. Must be 'strict', 'compatible', or 'ignore'.`);
59
+ throw new Error(`Invalid --schema-mode value: "${mode}". Valid modes are: strict, compatible, ignore`);
50
60
  }
51
61
  options.schemaMode = mode;
52
62
  }
@@ -78,7 +88,7 @@ async function main() {
78
88
  return;
79
89
  }
80
90
  if (args.includes('--help') || args.includes('-h')) {
81
- const program = createProgram();
91
+ const program = createTopLevelProgram();
82
92
  await program.parseAsync(process.argv);
83
93
  return;
84
94
  }
@@ -90,34 +100,23 @@ async function main() {
90
100
  console.error(formatHumanError(error, false));
91
101
  process.exit(1);
92
102
  }
93
- const cleanArg = args.find((arg) => arg === '--clean' || arg.startsWith('--clean='));
94
- if (cleanArg) {
95
- const options = extractOptions(args);
96
- if (options.verbose)
97
- setVerbose(true);
98
- if (options.json)
99
- setJsonMode(true);
100
- const cleanValue = cleanArg.includes('=') ? cleanArg.split('=')[1] : '';
101
- const cleanTypes = cleanValue ? cleanValue.split(',').map((s) => s.trim()) : [];
102
- try {
103
- validateCleanTypes(cleanTypes);
104
- }
105
- catch (error) {
106
- console.error(formatHumanError(error, false));
107
- process.exit(1);
103
+ let firstNonOption;
104
+ let firstNonOptionIndex = -1;
105
+ for (let i = 0; i < args.length; i++) {
106
+ const arg = args[i];
107
+ if (!arg)
108
+ continue;
109
+ if (arg.startsWith('-')) {
110
+ if (optionTakesValue(arg) && !arg.includes('=') && i + 1 < args.length) {
111
+ i++;
112
+ }
113
+ continue;
108
114
  }
109
- await clean({
110
- outputMode: options.json ? 'json' : 'human',
111
- sessions: cleanTypes.includes('sessions'),
112
- profiles: cleanTypes.includes('profiles'),
113
- logs: cleanTypes.includes('logs'),
114
- all: cleanTypes.includes('all'),
115
- });
116
- await closeFileLogger();
117
- return;
115
+ firstNonOption = arg;
116
+ firstNonOptionIndex = i;
117
+ break;
118
118
  }
119
- const targetInfo = findTarget(args);
120
- if (!targetInfo) {
119
+ if (!firstNonOption) {
121
120
  const { json } = extractOptions(args);
122
121
  if (json)
123
122
  setJsonMode(true);
@@ -128,240 +127,421 @@ async function main() {
128
127
  await closeFileLogger();
129
128
  return;
130
129
  }
131
- const { target, targetIndex } = targetInfo;
132
- const modifiedArgs = [
133
- ...process.argv.slice(0, 2),
134
- ...args.slice(0, targetIndex),
135
- ...args.slice(targetIndex + 1),
136
- ];
137
- if (target === 'x402') {
138
- const x402Args = args.slice(targetIndex + 1);
139
- await handleX402Command(x402Args);
140
- await closeFileLogger();
141
- return;
142
- }
143
- try {
144
- await handleCommands(target, modifiedArgs);
145
- }
146
- catch (error) {
147
- if (isMcpError(error)) {
148
- const opts = extractOptions(args);
149
- const outputMode = opts.json ? 'json' : 'human';
150
- if (outputMode === 'json') {
151
- console.error(formatJsonError(error, error.code));
130
+ if (firstNonOption.startsWith('@')) {
131
+ const session = firstNonOption;
132
+ const modifiedArgs = [
133
+ ...process.argv.slice(0, 2),
134
+ ...args.slice(0, firstNonOptionIndex),
135
+ ...args.slice(firstNonOptionIndex + 1),
136
+ ];
137
+ try {
138
+ await handleSessionCommands(session, modifiedArgs);
139
+ }
140
+ catch (error) {
141
+ if (isMcpError(error)) {
142
+ const opts = extractOptions(args);
143
+ const outputMode = opts.json ? 'json' : 'human';
144
+ if (outputMode === 'json') {
145
+ console.error(formatJsonError(error, error.code));
146
+ }
147
+ else {
148
+ console.error(formatHumanError(error, opts.verbose));
149
+ }
150
+ process.exit(error.code);
152
151
  }
153
- else {
154
- console.error(formatHumanError(error, opts.verbose));
155
- if (error instanceof NetworkError && KNOWN_COMMANDS.includes(target)) {
156
- console.error(`\nDid you mean "mcpc <target> ${target}" ?`);
157
- console.error(`Run "mcpc --help" for usage information.\n`);
158
- process.exit(error.code);
152
+ throw error;
153
+ }
154
+ finally {
155
+ await closeFileLogger();
156
+ }
157
+ await flushStdout();
158
+ process.exit(0);
159
+ }
160
+ if (KNOWN_COMMANDS.includes(firstNonOption)) {
161
+ if (firstNonOption === 'x402') {
162
+ const x402Args = args.slice(firstNonOptionIndex + 1);
163
+ await handleX402Command(x402Args);
164
+ await closeFileLogger();
165
+ return;
166
+ }
167
+ try {
168
+ const program = createTopLevelProgram();
169
+ await program.parseAsync(process.argv);
170
+ }
171
+ catch (error) {
172
+ if (isMcpError(error)) {
173
+ const opts = extractOptions(args);
174
+ const outputMode = opts.json ? 'json' : 'human';
175
+ if (outputMode === 'json') {
176
+ console.error(formatJsonError(error, error.code));
177
+ }
178
+ else {
179
+ console.error(formatHumanError(error, opts.verbose));
159
180
  }
181
+ process.exit(error.code);
160
182
  }
161
- process.exit(error.code);
183
+ throw error;
184
+ }
185
+ finally {
186
+ await closeFileLogger();
162
187
  }
163
- throw error;
188
+ return;
164
189
  }
165
- finally {
166
- await closeFileLogger();
190
+ const opts = extractOptions(args);
191
+ const outputMode = opts.json ? 'json' : 'human';
192
+ const allCommands = [...KNOWN_COMMANDS, ...KNOWN_SESSION_COMMANDS];
193
+ if (allCommands.includes(firstNonOption)) {
194
+ if (outputMode === 'json') {
195
+ console.error(formatJsonError(new Error(`Missing session target for command: ${firstNonOption}`), 1));
196
+ }
197
+ else {
198
+ console.error(`Error: Missing session target for command: ${firstNonOption}`);
199
+ console.error(`\nDid you mean: mcpc <@session> ${firstNonOption}`);
200
+ console.error(`Run "mcpc --help" for usage information.\n`);
201
+ }
167
202
  }
168
- await new Promise((resolve) => {
169
- if (process.stdout.writableFinished) {
170
- resolve();
203
+ else {
204
+ if (outputMode === 'json') {
205
+ console.error(formatJsonError(new Error(`Unknown command: ${firstNonOption}`), 1));
171
206
  }
172
207
  else {
173
- process.stdout.once('finish', resolve);
174
- process.stdout.end();
208
+ console.error(`Error: Unknown command: ${firstNonOption}`);
209
+ console.error(`Run "mcpc --help" for usage information.\n`);
175
210
  }
176
- });
177
- process.exit(0);
211
+ }
212
+ await closeFileLogger();
213
+ process.exit(1);
178
214
  }
179
- function createProgram() {
215
+ function createTopLevelProgram() {
180
216
  const program = new Command();
181
217
  program.configureOutput({
182
218
  outputError: (str, write) => write(str),
183
219
  getOutHelpWidth: () => 100,
184
220
  getErrHelpWidth: () => 100,
185
221
  });
222
+ program.configureHelp({
223
+ subcommandTerm: (cmd) => `${cmd.name()} ${cmd.usage()}`.replace(/^\[options\]\s*|\s*\[options\]/g, '').trim(),
224
+ });
225
+ const docsUrl = process.stdout.isTTY
226
+ ? `https://github.com/apify/mcpc/tree/v${mcpcVersion}`
227
+ : `https://raw.githubusercontent.com/apify/mcpc/v${mcpcVersion}/README.md`;
186
228
  program
187
229
  .name('mcpc')
188
230
  .description(`${rainbow('Universal')} command-line client for the Model Context Protocol (MCP).`)
189
- .usage('[options] <target> [command]')
190
- .helpOption('-h, --help', 'Display general help')
231
+ .usage('[options] [<@session>] [<command>]')
191
232
  .option('-j, --json', 'Output in JSON format for scripting')
192
- .option('-c, --config <file>', 'Path to MCP config JSON file (e.g. ".vscode/mcp.json")')
193
- .option('-H, --header <header>', 'HTTP header for remote MCP server (can be repeated)')
194
- .version(mcpcVersion, '-v, --version', 'Output the version number')
195
233
  .option('--verbose', 'Enable debug logging')
196
234
  .option('--profile <name>', 'OAuth profile for the server ("default" if not provided)')
197
235
  .option('--schema <file>', 'Validate tool/prompt schema against expected schema')
198
236
  .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
199
237
  .option('--timeout <seconds>', 'Request timeout in seconds (default: 300)')
200
- .option('--proxy <[host:]port>', 'Start proxy MCP server for session (with "connect" command)')
201
- .option('--proxy-bearer-token <token>', 'Require authentication for access to proxy server')
202
- .option('--x402', 'Enable x402 auto-payment using the configured wallet')
203
- .option('--clean[=types]', 'Clean up mcpc data (types: sessions, logs, profiles, all)');
204
- const docsUrl = process.stdout.isTTY
205
- ? `https://github.com/apify/mcpc/tree/v${mcpcVersion}`
206
- : `https://raw.githubusercontent.com/apify/mcpc/v${mcpcVersion}/README.md`;
238
+ .option('--insecure', 'Skip TLS certificate verification (for self-signed certs)')
239
+ .version(mcpcVersion, '-v, --version', 'Output the version number')
240
+ .helpOption('-h, --help', 'Display help');
207
241
  program.addHelpText('after', `
208
- Targets:
209
- @<session> Named persistent session (e.g. "@apify")
210
- <config-entry> Entry in MCP config file specified by --config (e.g. "fs")
211
- <server-url> Remote MCP server URL (e.g. "mcp.apify.com")
242
+ MCP session commands (after connecting):
243
+ <@session> Show MCP server info and capabilities
244
+ <@session> tools-list List MCP tools
245
+ <@session> tools-get <name>
246
+ <@session> tools-call <name> [arg:=val ... | <json> | <stdin]
247
+ <@session> prompts-list
248
+ <@session> prompts-get <name> [arg:=val ... | <json> | <stdin]
249
+ <@session> resources-list
250
+ <@session> resources-read <uri>
251
+ <@session> resources-subscribe <uri>
252
+ <@session> resources-unsubscribe <uri>
253
+ <@session> resources-templates-list
254
+ <@session> tasks-list
255
+ <@session> tasks-get <taskId>
256
+ <@session> tasks-cancel <taskId>
257
+ <@session> logging-set-level <level>
258
+ <@session> ping
212
259
 
213
- Management commands:
214
- login Create OAuth profile with credentials for remote server
215
- logout Remove OAuth profile for remote server
216
- connect @<session> Connect to server and create named persistent session
217
- restart Kill and restart a session
218
- close Close a session
219
-
220
- MCP server commands:
221
- help Show server info ("help" can be omitted)
222
- shell Open interactive shell
223
- tools-list [--full] Send "tools/list" MCP request...
224
- tools-get <tool-name>
225
- tools-call <tool-name> [arg1:=val1 arg2:=val2 ... | <args-json> | <stdin]
226
- prompts-list
227
- prompts-get <prompt-name> [arg1:=val1 arg2:=val2 ... | <args-json> | <stdin]
228
- resources
229
- resources-list
230
- resources-read <uri>
231
- resources-subscribe <uri>
232
- resources-unsubscribe <uri>
233
- resources-templates-list
234
- logging-set-level <level>
235
- ping
236
-
237
- x402 payment commands (no target needed):
238
- x402 init Create a new x402 wallet
239
- x402 import <key> Import wallet from private key
240
- x402 info Show wallet info
241
- x402 sign -r <base64> Sign payment from PAYMENT-REQUIRED header
242
- x402 remove Remove the wallet
243
-
244
- Run "mcpc" without <target> to show available sessions and profiles.
260
+ Run "mcpc" without arguments to show active sessions and OAuth profiles.
245
261
 
246
262
  Full docs: ${docsUrl}`);
263
+ program
264
+ .command('connect [server] [@session]')
265
+ .usage('<server> <@session>')
266
+ .description('Connect to an MCP server and start a new named @session')
267
+ .option('-H, --header <header>', 'HTTP header (can be repeated)')
268
+ .option('--profile <name>', 'OAuth profile to use ("default" if skipped)')
269
+ .option('--no-profile', 'Skip OAuth profile (connect anonymously)')
270
+ .option('--proxy <[host:]port>', 'Start proxy MCP server for session')
271
+ .option('--proxy-bearer-token <token>', 'Require authentication for access to proxy server')
272
+ .option('--x402', 'Enable x402 auto-payment using the configured wallet')
273
+ .addHelpText('after', `
274
+ Server formats:
275
+ mcp.apify.com Remote HTTP server (https:// added automatically)
276
+ ~/.vscode/mcp.json:puppeteer Config file entry (file:entry)
277
+ `)
278
+ .action(async (server, sessionName, opts, command) => {
279
+ if (!server) {
280
+ throw new ClientError('Missing required argument: server\n\nExample: mcpc connect mcp.apify.com @myapp');
281
+ }
282
+ if (!sessionName) {
283
+ throw new ClientError('Missing required argument: @session\n\nExample: mcpc connect mcp.apify.com @myapp');
284
+ }
285
+ const globalOpts = getOptionsFromCommand(command);
286
+ const parsed = parseServerArg(server);
287
+ const headers = opts.header
288
+ ? Array.isArray(opts.header)
289
+ ? opts.header
290
+ : [opts.header]
291
+ : undefined;
292
+ if (!parsed) {
293
+ throw new ClientError(`Invalid server: "${server}"\n\n` +
294
+ `Expected a URL (e.g. mcp.apify.com) or a config file entry (e.g. ~/.vscode/mcp.json:filesystem)`);
295
+ }
296
+ if (parsed.type === 'config') {
297
+ await sessions.connectSession(parsed.entry, sessionName, {
298
+ ...globalOpts,
299
+ ...(headers && { headers }),
300
+ config: parsed.file,
301
+ proxy: opts.proxy,
302
+ proxyBearerToken: opts.proxyBearerToken,
303
+ x402: opts.x402,
304
+ ...(globalOpts.insecure && { insecure: true }),
305
+ });
306
+ }
307
+ else {
308
+ await sessions.connectSession(server, sessionName, {
309
+ ...globalOpts,
310
+ ...(headers && { headers }),
311
+ proxy: opts.proxy,
312
+ proxyBearerToken: opts.proxyBearerToken,
313
+ x402: opts.x402,
314
+ ...(globalOpts.insecure && { insecure: true }),
315
+ });
316
+ }
317
+ });
318
+ program
319
+ .command('close [@session]')
320
+ .usage('<@session>')
321
+ .description('Close a session')
322
+ .action(async (sessionName, _opts, command) => {
323
+ if (!sessionName) {
324
+ throw new ClientError('Missing required argument: @session\n\nExample: mcpc close @myapp');
325
+ }
326
+ await sessions.closeSession(sessionName, getOptionsFromCommand(command));
327
+ });
328
+ program
329
+ .command('restart [@session]')
330
+ .usage('<@session>')
331
+ .description('Restart a session (losing all state)')
332
+ .action(async (sessionName, _opts, command) => {
333
+ if (!sessionName) {
334
+ throw new ClientError('Missing required argument: @session\n\nExample: mcpc restart @myapp');
335
+ }
336
+ await sessions.restartSession(sessionName, getOptionsFromCommand(command));
337
+ });
338
+ program
339
+ .command('shell [@session]')
340
+ .usage('<@session>')
341
+ .description('Open interactive shell for a session')
342
+ .action(async (sessionName) => {
343
+ if (!sessionName) {
344
+ throw new ClientError('Missing required argument: @session\n\nExample: mcpc shell @myapp');
345
+ }
346
+ await sessions.openShell(sessionName);
347
+ });
348
+ program
349
+ .command('login [server]')
350
+ .usage('<server>')
351
+ .description('Interactively login to a server using OAuth and save profile')
352
+ .option('--profile <name>', 'Profile name (default: "default")')
353
+ .option('--scope <scopes>', 'OAuth scopes to request, quoted and space-separated (e.g. --scope "read write")')
354
+ .option('--client-id <id>', 'OAuth client ID (for servers without dynamic client registration)')
355
+ .option('--client-secret <secret>', 'OAuth client secret (for servers without dynamic client registration)')
356
+ .action(async (server, opts, command) => {
357
+ if (!server) {
358
+ throw new ClientError('Missing required argument: server\n\nExample: mcpc login mcp.apify.com');
359
+ }
360
+ await auth.login(server, {
361
+ profile: opts.profile,
362
+ scope: opts.scope,
363
+ clientId: opts.clientId,
364
+ clientSecret: opts.clientSecret,
365
+ ...getOptionsFromCommand(command),
366
+ });
367
+ });
368
+ program
369
+ .command('logout [server]')
370
+ .usage('<server>')
371
+ .description('Delete an authentication profile for a server')
372
+ .option('--profile <name>', 'Profile name (default: "default")')
373
+ .action(async (server, opts, command) => {
374
+ if (!server) {
375
+ throw new ClientError('Missing required argument: server\n\nExample: mcpc logout mcp.apify.com');
376
+ }
377
+ await auth.logout(server, {
378
+ profile: opts.profile,
379
+ ...getOptionsFromCommand(command),
380
+ });
381
+ });
382
+ program
383
+ .command('clean [resources...]')
384
+ .description('Clean up mcpc data (sessions, profiles, logs, all)')
385
+ .addHelpText('after', `
386
+ Resources:
387
+ sessions Remove stale/crashed session records
388
+ profiles Remove authentication profiles
389
+ logs Remove bridge log files
390
+ all Remove all of the above
391
+
392
+ Without arguments, performs safe cleanup of stale data only.
393
+ `)
394
+ .action(async (resources, _opts, command) => {
395
+ const globalOpts = getOptionsFromCommand(command);
396
+ const VALID_CLEAN_TYPES = ['sessions', 'profiles', 'logs', 'all'];
397
+ for (const r of resources) {
398
+ if (!VALID_CLEAN_TYPES.includes(r)) {
399
+ throw new ClientError(`Invalid clean resource: "${r}". Valid resources are: ${VALID_CLEAN_TYPES.join(', ')}`);
400
+ }
401
+ }
402
+ await clean({
403
+ outputMode: globalOpts.outputMode,
404
+ sessions: resources.includes('sessions'),
405
+ profiles: resources.includes('profiles'),
406
+ logs: resources.includes('logs'),
407
+ all: resources.includes('all'),
408
+ });
409
+ });
410
+ program
411
+ .command('x402 [subcommand] [args...]')
412
+ .description('Configure an x402 payment wallet (EXPERIMENTAL)')
413
+ .addHelpText('after', `
414
+ Subcommands:
415
+ init Create a new x402 wallet
416
+ import <key> Import wallet from private key
417
+ info Show wallet info
418
+ sign -r <b64> Sign payment from PAYMENT-REQUIRED header
419
+ remove Remove the wallet
420
+ `)
421
+ .action(() => { });
422
+ program
423
+ .command('help [command]')
424
+ .description('Show help for a specific command')
425
+ .action((cmdName) => {
426
+ if (!cmdName) {
427
+ program.outputHelp();
428
+ return;
429
+ }
430
+ const topLevelCmd = program.commands.find((c) => c.name() === cmdName || c.aliases().includes(cmdName));
431
+ if (topLevelCmd) {
432
+ topLevelCmd.outputHelp();
433
+ return;
434
+ }
435
+ const dummyProgram = new Command();
436
+ registerSessionCommands(dummyProgram, '@dummy');
437
+ const sessionCmd = dummyProgram.commands.find((c) => c.name() === cmdName || c.aliases().includes(cmdName));
438
+ if (sessionCmd) {
439
+ sessionCmd.outputHelp();
440
+ return;
441
+ }
442
+ console.error(`Unknown command: ${cmdName}`);
443
+ console.error(`Run "mcpc --help" for usage information.`);
444
+ process.exit(1);
445
+ });
446
+ program.action(async () => {
447
+ const opts = program.opts();
448
+ const json = opts.json || getJsonFromEnv();
449
+ if (json)
450
+ setJsonMode(true);
451
+ await sessions.listSessionsAndAuthProfiles({ outputMode: json ? 'json' : 'human' });
452
+ if (!json) {
453
+ console.log('\nRun "mcpc --help" for usage information.\n');
454
+ }
455
+ });
247
456
  return program;
248
457
  }
249
- async function handleCommands(target, args) {
250
- const program = createProgram();
251
- program.argument('<target>', 'Target (session @name, MCP config entry, or server URL)');
252
- if (!hasCommandAfterTarget(args)) {
253
- const options = extractOptions(args);
254
- if (options.verbose)
255
- setVerbose(true);
256
- if (options.json)
257
- setJsonMode(true);
258
- await sessions.showServerDetails(target, {
259
- outputMode: options.json ? 'json' : 'human',
260
- ...(options.verbose && { verbose: true }),
261
- ...(options.config && { config: options.config }),
262
- ...(options.headers && { headers: options.headers }),
263
- ...(options.timeout !== undefined && { timeout: options.timeout }),
264
- });
265
- return;
266
- }
458
+ function registerSessionCommands(program, session) {
267
459
  program
268
460
  .command('help')
269
461
  .description('Show server instructions and available capabilities')
270
462
  .action(async (_options, command) => {
271
- await sessions.showHelp(target, getOptionsFromCommand(command));
463
+ await sessions.showHelp(session, getOptionsFromCommand(command));
272
464
  });
273
465
  program
274
466
  .command('shell')
275
- .description('Interactive shell for the target')
467
+ .description('Interactive shell for the session')
276
468
  .action(async () => {
277
- await sessions.openShell(target);
469
+ await sessions.openShell(session);
278
470
  });
279
471
  program
280
- .command('close')
472
+ .command('close', { hidden: true })
281
473
  .description('Close the session')
282
474
  .action(async (_options, command) => {
283
- await sessions.closeSession(target, getOptionsFromCommand(command));
475
+ await sessions.closeSession(session, getOptionsFromCommand(command));
284
476
  });
285
477
  program
286
478
  .command('restart')
287
479
  .description('Restart the session (stop and start the bridge)')
288
480
  .action(async (_options, command) => {
289
- await sessions.restartSession(target, getOptionsFromCommand(command));
290
- });
291
- program
292
- .command('connect <name>')
293
- .description('Create or reconnect a named session to an MCP server')
294
- .action(async (name, _options, command) => {
295
- const opts = command.optsWithGlobals();
296
- await sessions.connectSession(name, target, {
297
- ...getOptionsFromCommand(command),
298
- proxy: opts.proxy,
299
- proxyBearerToken: opts.proxyBearerToken,
300
- x402: opts.x402,
301
- });
302
- });
303
- program
304
- .command('login')
305
- .description('Login to a server using OAuth and save authentication profile')
306
- .option('--profile <name>', 'Profile name (default: default)')
307
- .option('--scope <scope>', 'OAuth scope(s) to request')
308
- .action(async (options, command) => {
309
- await auth.login(target, {
310
- profile: options.profile,
311
- scope: options.scope,
312
- ...getOptionsFromCommand(command),
313
- });
314
- });
315
- program
316
- .command('logout')
317
- .description('Delete an authentication profile')
318
- .option('--profile <name>', 'Profile name (default: default)')
319
- .action(async (options, command) => {
320
- await auth.logout(target, {
321
- profile: options.profile,
322
- ...getOptionsFromCommand(command),
323
- });
481
+ await sessions.restartSession(session, getOptionsFromCommand(command));
324
482
  });
325
483
  program
326
484
  .command('tools')
327
485
  .description('List available tools (shorthand for tools-list)')
328
486
  .option('--full', 'Show full tool details including complete input schema')
329
487
  .action(async (_options, command) => {
330
- await tools.listTools(target, getOptionsFromCommand(command));
488
+ await tools.listTools(session, getOptionsFromCommand(command));
331
489
  });
332
490
  program
333
491
  .command('tools-list')
334
492
  .description('List available tools')
335
493
  .option('--full', 'Show full tool details including complete input schema')
336
494
  .action(async (_options, command) => {
337
- await tools.listTools(target, getOptionsFromCommand(command));
495
+ await tools.listTools(session, getOptionsFromCommand(command));
338
496
  });
339
497
  program
340
498
  .command('tools-get <name>')
341
499
  .description('Get information about a specific tool')
342
500
  .action(async (name, _options, command) => {
343
- await tools.getTool(target, name, getOptionsFromCommand(command));
501
+ await tools.getTool(session, name, getOptionsFromCommand(command));
344
502
  });
345
503
  program
346
504
  .command('tools-call <name> [args...]')
347
505
  .description('Call a tool with arguments (key:=value pairs or JSON)')
348
- .action(async (name, args, _options, command) => {
349
- await tools.callTool(target, name, {
506
+ .option('--async', 'Use async task execution (experimental)')
507
+ .option('--detach', 'Start async task and return immediately with task ID (implies --async)')
508
+ .action(async (name, args, options, command) => {
509
+ await tools.callTool(session, name, {
350
510
  args,
511
+ async: options.async,
512
+ detach: options.detach,
351
513
  ...getOptionsFromCommand(command),
352
514
  });
353
515
  });
516
+ program
517
+ .command('tasks-list')
518
+ .description('List active tasks')
519
+ .action(async (_options, command) => {
520
+ await tasks.listTasks(session, getOptionsFromCommand(command));
521
+ });
522
+ program
523
+ .command('tasks-get <taskId>')
524
+ .description('Get status of a specific task')
525
+ .action(async (taskId, _options, command) => {
526
+ await tasks.getTask(session, taskId, getOptionsFromCommand(command));
527
+ });
528
+ program
529
+ .command('tasks-cancel <taskId>')
530
+ .description('Cancel a running task')
531
+ .action(async (taskId, _options, command) => {
532
+ await tasks.cancelTask(session, taskId, getOptionsFromCommand(command));
533
+ });
354
534
  program
355
535
  .command('resources')
356
536
  .description('List available resources (shorthand for resources-list)')
357
537
  .action(async (_options, command) => {
358
- await resources.listResources(target, getOptionsFromCommand(command));
538
+ await resources.listResources(session, getOptionsFromCommand(command));
359
539
  });
360
540
  program
361
541
  .command('resources-list')
362
542
  .description('List available resources')
363
543
  .action(async (_options, command) => {
364
- await resources.listResources(target, getOptionsFromCommand(command));
544
+ await resources.listResources(session, getOptionsFromCommand(command));
365
545
  });
366
546
  program
367
547
  .command('resources-read <uri>')
@@ -369,9 +549,8 @@ async function handleCommands(target, args) {
369
549
  .option('-o, --output <file>', 'Write resource to file')
370
550
  .option('--max-size <bytes>', 'Maximum resource size in bytes')
371
551
  .action(async (uri, options, command) => {
372
- await resources.getResource(target, uri, {
552
+ await resources.getResource(session, uri, {
373
553
  output: options.output,
374
- raw: options.raw,
375
554
  maxSize: options.maxSize,
376
555
  ...getOptionsFromCommand(command),
377
556
  });
@@ -380,37 +559,37 @@ async function handleCommands(target, args) {
380
559
  .command('resources-subscribe <uri>')
381
560
  .description('Subscribe to resource updates')
382
561
  .action(async (uri, _options, command) => {
383
- await resources.subscribeResource(target, uri, getOptionsFromCommand(command));
562
+ await resources.subscribeResource(session, uri, getOptionsFromCommand(command));
384
563
  });
385
564
  program
386
565
  .command('resources-unsubscribe <uri>')
387
566
  .description('Unsubscribe from resource updates')
388
567
  .action(async (uri, _options, command) => {
389
- await resources.unsubscribeResource(target, uri, getOptionsFromCommand(command));
568
+ await resources.unsubscribeResource(session, uri, getOptionsFromCommand(command));
390
569
  });
391
570
  program
392
571
  .command('resources-templates-list')
393
572
  .description('List available resource templates')
394
573
  .action(async (_options, command) => {
395
- await resources.listResourceTemplates(target, getOptionsFromCommand(command));
574
+ await resources.listResourceTemplates(session, getOptionsFromCommand(command));
396
575
  });
397
576
  program
398
577
  .command('prompts')
399
578
  .description('List available prompts (shorthand for prompts-list)')
400
579
  .action(async (_options, command) => {
401
- await prompts.listPrompts(target, getOptionsFromCommand(command));
580
+ await prompts.listPrompts(session, getOptionsFromCommand(command));
402
581
  });
403
582
  program
404
583
  .command('prompts-list')
405
584
  .description('List available prompts')
406
585
  .action(async (_options, command) => {
407
- await prompts.listPrompts(target, getOptionsFromCommand(command));
586
+ await prompts.listPrompts(session, getOptionsFromCommand(command));
408
587
  });
409
588
  program
410
589
  .command('prompts-get <name> [args...]')
411
590
  .description('Get a prompt by name with arguments (key:=value pairs or JSON)')
412
591
  .action(async (name, args, _options, command) => {
413
- await prompts.getPrompt(target, name, {
592
+ await prompts.getPrompt(session, name, {
414
593
  args,
415
594
  ...getOptionsFromCommand(command),
416
595
  });
@@ -419,14 +598,50 @@ async function handleCommands(target, args) {
419
598
  .command('logging-set-level <level>')
420
599
  .description('Set server logging level (debug, info, notice, warning, error, critical, alert, emergency)')
421
600
  .action(async (level, _options, command) => {
422
- await logging.setLogLevel(target, level, getOptionsFromCommand(command));
601
+ await logging.setLogLevel(session, level, getOptionsFromCommand(command));
423
602
  });
424
603
  program
425
604
  .command('ping')
426
605
  .description('Ping the MCP server to check if it is alive')
427
606
  .action(async (_options, command) => {
428
- await utilities.ping(target, getOptionsFromCommand(command));
607
+ await utilities.ping(session, getOptionsFromCommand(command));
429
608
  });
609
+ }
610
+ function createSessionProgram() {
611
+ const program = new Command();
612
+ program.configureOutput({
613
+ outputError: (str, write) => write(str),
614
+ getOutHelpWidth: () => 100,
615
+ getErrHelpWidth: () => 100,
616
+ });
617
+ program
618
+ .name('mcpc <@session>')
619
+ .helpOption('-h, --help', 'Display help')
620
+ .option('-j, --json', 'Output in JSON format for scripting and code mode')
621
+ .option('--verbose', 'Enable debug logging')
622
+ .option('--profile <name>', 'OAuth profile override')
623
+ .option('--schema <file>', 'Validate tool/prompt schema against expected schema')
624
+ .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
625
+ .option('--timeout <seconds>', 'Request timeout in seconds (default: 300)')
626
+ .option('--insecure', 'Skip TLS certificate verification (for self-signed certs)');
627
+ return program;
628
+ }
629
+ async function handleSessionCommands(session, args) {
630
+ if (!hasSubcommand(args)) {
631
+ const options = extractOptions(args);
632
+ if (options.verbose)
633
+ setVerbose(true);
634
+ if (options.json)
635
+ setJsonMode(true);
636
+ await sessions.showServerDetails(session, {
637
+ outputMode: options.json ? 'json' : 'human',
638
+ ...(options.verbose && { verbose: true }),
639
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
640
+ });
641
+ return;
642
+ }
643
+ const program = createSessionProgram();
644
+ registerSessionCommands(program, session);
430
645
  try {
431
646
  await program.parseAsync(args);
432
647
  }
@@ -448,6 +663,17 @@ async function handleCommands(target, args) {
448
663
  process.exit(1);
449
664
  }
450
665
  }
666
+ async function flushStdout() {
667
+ await new Promise((resolve) => {
668
+ if (process.stdout.writableFinished) {
669
+ resolve();
670
+ }
671
+ else {
672
+ process.stdout.once('finish', resolve);
673
+ process.stdout.end();
674
+ }
675
+ });
676
+ }
451
677
  main().catch(async (error) => {
452
678
  console.error('Fatal error:', error);
453
679
  await closeFileLogger();