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

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