@apify/mcpc 0.2.4 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/CONTRIBUTING.md +12 -0
  3. package/NOTICE +27 -0
  4. package/README.md +219 -226
  5. package/_config.yml +30 -0
  6. package/client-logo.svg +79 -0
  7. package/client-metadata.json +16 -0
  8. package/dist/bridge/index.js +51 -4
  9. package/dist/bridge/index.js.map +1 -1
  10. package/dist/cli/commands/auth.d.ts +2 -0
  11. package/dist/cli/commands/auth.d.ts.map +1 -1
  12. package/dist/cli/commands/auth.js +32 -10
  13. package/dist/cli/commands/auth.js.map +1 -1
  14. package/dist/cli/commands/clean.d.ts.map +1 -1
  15. package/dist/cli/commands/clean.js +13 -2
  16. package/dist/cli/commands/clean.js.map +1 -1
  17. package/dist/cli/commands/grep.d.ts.map +1 -1
  18. package/dist/cli/commands/grep.js +39 -8
  19. package/dist/cli/commands/grep.js.map +1 -1
  20. package/dist/cli/commands/prompts.d.ts.map +1 -1
  21. package/dist/cli/commands/prompts.js +7 -26
  22. package/dist/cli/commands/prompts.js.map +1 -1
  23. package/dist/cli/commands/resources.d.ts.map +1 -1
  24. package/dist/cli/commands/resources.js +9 -3
  25. package/dist/cli/commands/resources.js.map +1 -1
  26. package/dist/cli/commands/sessions.d.ts +45 -2
  27. package/dist/cli/commands/sessions.d.ts.map +1 -1
  28. package/dist/cli/commands/sessions.js +493 -27
  29. package/dist/cli/commands/sessions.js.map +1 -1
  30. package/dist/cli/commands/tasks.d.ts +1 -0
  31. package/dist/cli/commands/tasks.d.ts.map +1 -1
  32. package/dist/cli/commands/tasks.js +15 -1
  33. package/dist/cli/commands/tasks.js.map +1 -1
  34. package/dist/cli/commands/tools.d.ts +6 -1
  35. package/dist/cli/commands/tools.d.ts.map +1 -1
  36. package/dist/cli/commands/tools.js +66 -14
  37. package/dist/cli/commands/tools.js.map +1 -1
  38. package/dist/cli/commands/x402.d.ts.map +1 -1
  39. package/dist/cli/commands/x402.js +7 -7
  40. package/dist/cli/commands/x402.js.map +1 -1
  41. package/dist/cli/helpers.d.ts.map +1 -1
  42. package/dist/cli/helpers.js +3 -6
  43. package/dist/cli/helpers.js.map +1 -1
  44. package/dist/cli/index.js +370 -131
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/cli/output.d.ts +18 -5
  47. package/dist/cli/output.d.ts.map +1 -1
  48. package/dist/cli/output.js +275 -89
  49. package/dist/cli/output.js.map +1 -1
  50. package/dist/cli/parser.d.ts +4 -0
  51. package/dist/cli/parser.d.ts.map +1 -1
  52. package/dist/cli/parser.js +68 -24
  53. package/dist/cli/parser.js.map +1 -1
  54. package/dist/cli/shell.d.ts.map +1 -1
  55. package/dist/cli/shell.js +44 -21
  56. package/dist/cli/shell.js.map +1 -1
  57. package/dist/cli/tool-result.d.ts +1 -1
  58. package/dist/cli/tool-result.d.ts.map +1 -1
  59. package/dist/cli/tool-result.js +20 -15
  60. package/dist/cli/tool-result.js.map +1 -1
  61. package/dist/core/factory.d.ts +1 -0
  62. package/dist/core/factory.d.ts.map +1 -1
  63. package/dist/core/factory.js +3 -0
  64. package/dist/core/factory.js.map +1 -1
  65. package/dist/core/mcp-client.d.ts +1 -0
  66. package/dist/core/mcp-client.d.ts.map +1 -1
  67. package/dist/core/mcp-client.js +14 -0
  68. package/dist/core/mcp-client.js.map +1 -1
  69. package/dist/core/transports.d.ts +5 -1
  70. package/dist/core/transports.d.ts.map +1 -1
  71. package/dist/core/transports.js +26 -4
  72. package/dist/core/transports.js.map +1 -1
  73. package/dist/lib/auth/auth-page.d.ts +13 -0
  74. package/dist/lib/auth/auth-page.d.ts.map +1 -0
  75. package/dist/lib/auth/auth-page.js +129 -0
  76. package/dist/lib/auth/auth-page.js.map +1 -0
  77. package/dist/lib/auth/oauth-flow.d.ts +2 -1
  78. package/dist/lib/auth/oauth-flow.d.ts.map +1 -1
  79. package/dist/lib/auth/oauth-flow.js +65 -58
  80. package/dist/lib/auth/oauth-flow.js.map +1 -1
  81. package/dist/lib/auth/oauth-provider.d.ts +2 -0
  82. package/dist/lib/auth/oauth-provider.d.ts.map +1 -1
  83. package/dist/lib/auth/oauth-provider.js +6 -0
  84. package/dist/lib/auth/oauth-provider.js.map +1 -1
  85. package/dist/lib/auth/oauth-utils.d.ts +3 -0
  86. package/dist/lib/auth/oauth-utils.d.ts.map +1 -1
  87. package/dist/lib/auth/oauth-utils.js +32 -1
  88. package/dist/lib/auth/oauth-utils.js.map +1 -1
  89. package/dist/lib/auth/profiles.d.ts.map +1 -1
  90. package/dist/lib/auth/profiles.js +3 -3
  91. package/dist/lib/auth/profiles.js.map +1 -1
  92. package/dist/lib/bridge-manager.d.ts.map +1 -1
  93. package/dist/lib/bridge-manager.js +43 -28
  94. package/dist/lib/bridge-manager.js.map +1 -1
  95. package/dist/lib/cleanup.d.ts +5 -0
  96. package/dist/lib/cleanup.d.ts.map +1 -1
  97. package/dist/lib/cleanup.js +38 -1
  98. package/dist/lib/cleanup.js.map +1 -1
  99. package/dist/lib/config.d.ts +21 -0
  100. package/dist/lib/config.d.ts.map +1 -1
  101. package/dist/lib/config.js +99 -5
  102. package/dist/lib/config.js.map +1 -1
  103. package/dist/lib/errors.d.ts +1 -0
  104. package/dist/lib/errors.d.ts.map +1 -1
  105. package/dist/lib/errors.js +4 -1
  106. package/dist/lib/errors.js.map +1 -1
  107. package/dist/lib/session-client.d.ts +1 -0
  108. package/dist/lib/session-client.d.ts.map +1 -1
  109. package/dist/lib/session-client.js +7 -4
  110. package/dist/lib/session-client.js.map +1 -1
  111. package/dist/lib/sessions.d.ts.map +1 -1
  112. package/dist/lib/sessions.js +18 -9
  113. package/dist/lib/sessions.js.map +1 -1
  114. package/dist/lib/types.d.ts +2 -0
  115. package/dist/lib/types.d.ts.map +1 -1
  116. package/dist/lib/utils.d.ts +16 -2
  117. package/dist/lib/utils.d.ts.map +1 -1
  118. package/dist/lib/utils.js +112 -8
  119. package/dist/lib/utils.js.map +1 -1
  120. package/dist/lib/wallets.js +3 -3
  121. package/dist/lib/wallets.js.map +1 -1
  122. package/docs/TODOs.md +5 -0
  123. package/package.json +7 -6
package/dist/cli/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { initProxy } from '../lib/proxy.js';
3
- import { Command, Help } from 'commander';
3
+ import { Command, CommanderError, Help } from 'commander';
4
4
  import { setVerbose, setJsonMode, closeFileLogger } from '../lib/index.js';
5
5
  import { isMcpError, formatHumanError, ClientError } from '../lib/index.js';
6
6
  import chalk from 'chalk';
7
- import { formatJson, formatJsonError, rainbow } from './output.js';
7
+ import { formatJson, formatJsonError, rainbow, theme } from './output.js';
8
8
  import * as tools from './commands/tools.js';
9
9
  import * as resources from './commands/resources.js';
10
10
  import * as prompts from './commands/prompts.js';
@@ -16,7 +16,7 @@ import * as tasks from './commands/tasks.js';
16
16
  import * as grepCmd from './commands/grep.js';
17
17
  import { handleX402Command } from './commands/x402.js';
18
18
  import { clean } from './commands/clean.js';
19
- import { extractOptions, getVerboseFromEnv, getJsonFromEnv, validateOptions, validateArgValues, parseServerArg, hasSubcommand, optionTakesValue, KNOWN_COMMANDS, KNOWN_SESSION_COMMANDS, } from './parser.js';
19
+ import { extractOptions, getVerboseFromEnv, getJsonFromEnv, validateOptions, validateArgValues, parseServerArg, hasSubcommand, optionTakesValue, suggestCommand, KNOWN_COMMANDS, KNOWN_SESSION_COMMANDS, } from './parser.js';
20
20
  import { createRequire } from 'module';
21
21
  const { version: mcpcVersion } = createRequire(import.meta.url)('../../package.json');
22
22
  {
@@ -64,8 +64,21 @@ function getOptionsFromCommand(command) {
64
64
  }
65
65
  if (opts.full)
66
66
  options.full = opts.full;
67
+ if (opts.maxChars) {
68
+ const maxChars = parseInt(opts.maxChars, 10);
69
+ if (isNaN(maxChars) || maxChars <= 0) {
70
+ throw new Error(`Invalid --max-chars value: "${opts.maxChars}". Must be a positive number (characters).`);
71
+ }
72
+ options.maxChars = maxChars;
73
+ }
67
74
  return options;
68
75
  }
76
+ function jsonHelp(description, shape, schemaUrl) {
77
+ const line = shape ? ` ${description}:\n ${shape}` : ` ${description}`;
78
+ const link = schemaUrl ? `\n Schema: ${schemaUrl}` : '';
79
+ return `\n${chalk.bold('JSON output (--json):')}\n${line}${link}\n`;
80
+ }
81
+ const SCHEMA_BASE = 'https://modelcontextprotocol.io/specification/2025-11-25/schema';
69
82
  async function main() {
70
83
  const args = process.argv.slice(2);
71
84
  const handleExit = () => {
@@ -90,23 +103,33 @@ async function main() {
90
103
  return;
91
104
  }
92
105
  if (args.includes('--help') || args.includes('-h')) {
93
- if (args.includes('x402')) {
106
+ const hasSessionArg = args.some((a) => a.startsWith('@') && !a.startsWith('--'));
107
+ if (hasSessionArg) {
108
+ }
109
+ else if (args.includes('x402')) {
94
110
  const x402Index = args.indexOf('x402');
95
111
  const x402Args = args.slice(x402Index + 1);
96
112
  await handleX402Command(x402Args);
97
113
  await closeFileLogger();
98
114
  return;
99
115
  }
100
- const program = createTopLevelProgram();
101
- await program.parseAsync(process.argv);
102
- return;
116
+ else {
117
+ const helpTarget = args.find((a) => a !== '--help' && a !== '-h' && !a.startsWith('-') && !a.startsWith('@'));
118
+ if (helpTarget && KNOWN_SESSION_COMMANDS.includes(helpTarget)) {
119
+ showSessionCommandHelp(helpTarget);
120
+ return;
121
+ }
122
+ const program = createTopLevelProgram();
123
+ await program.parseAsync(process.argv);
124
+ return;
125
+ }
103
126
  }
104
127
  try {
105
128
  validateOptions(args);
106
129
  validateArgValues(args);
107
130
  }
108
131
  catch (error) {
109
- console.error(chalk.red(formatHumanError(error, false)));
132
+ console.error(theme.red(formatHumanError(error, false)));
110
133
  process.exit(1);
111
134
  }
112
135
  let firstNonOption;
@@ -154,7 +177,7 @@ async function main() {
154
177
  console.error(formatJsonError(error, error.code));
155
178
  }
156
179
  else {
157
- console.error(chalk.red(formatHumanError(error, opts.verbose)));
180
+ console.error(theme.red(formatHumanError(error, opts.verbose)));
158
181
  }
159
182
  process.exit(error.code);
160
183
  }
@@ -185,7 +208,7 @@ async function main() {
185
208
  console.error(formatJsonError(error, error.code));
186
209
  }
187
210
  else {
188
- console.error(chalk.red(formatHumanError(error, opts.verbose)));
211
+ console.error(theme.red(formatHumanError(error, opts.verbose)));
189
212
  }
190
213
  process.exit(error.code);
191
214
  }
@@ -210,11 +233,20 @@ async function main() {
210
233
  }
211
234
  }
212
235
  else {
236
+ const suggestion = suggestCommand(firstNonOption, allCommands);
213
237
  if (outputMode === 'json') {
214
238
  console.error(formatJsonError(new Error(`Unknown command: ${firstNonOption}`), 1));
215
239
  }
216
240
  else {
217
241
  console.error(`Error: Unknown command: ${firstNonOption}`);
242
+ if (suggestion) {
243
+ if (KNOWN_SESSION_COMMANDS.includes(suggestion)) {
244
+ console.error(`\nDid you mean: mcpc <@session> ${suggestion}`);
245
+ }
246
+ else {
247
+ console.error(`\nDid you mean: mcpc ${suggestion}`);
248
+ }
249
+ }
218
250
  console.error(`Run "mcpc --help" for usage information.\n`);
219
251
  }
220
252
  }
@@ -231,7 +263,7 @@ function createTopLevelProgram() {
231
263
  program.configureHelp({
232
264
  subcommandTerm: (cmd) => `${cmd.name()} ${cmd.usage()}`.replace(/^\[options\]\s*|\s*\[options\]/g, '').trim(),
233
265
  styleTitle: (str) => chalk.bold(str),
234
- styleSubcommandText: (str) => chalk.cyan(str),
266
+ styleSubcommandText: (str) => theme.cyan(str),
235
267
  formatHelp: (cmd, helper) => {
236
268
  const output = Help.prototype.formatHelp.call(helper, cmd, helper);
237
269
  const sections = output.split('\n\n');
@@ -248,78 +280,131 @@ function createTopLevelProgram() {
248
280
  .join('\n\n') + '\n');
249
281
  },
250
282
  });
251
- const docsUrl = process.stdout.isTTY
252
- ? `https://github.com/apify/mcpc/tree/v${mcpcVersion}`
253
- : `https://raw.githubusercontent.com/apify/mcpc/v${mcpcVersion}/README.md`;
283
+ const docsUrl = `https://github.com/apify/mcpc/raw/refs/tags/v${mcpcVersion}/README.md`;
254
284
  program
255
285
  .name('mcpc')
256
286
  .description(`${rainbow('Universal')} command-line client for the Model Context Protocol (MCP).`)
257
287
  .usage('[<@session>] [<command>] [options]')
258
- .option('-j, --json', 'Output in JSON format for scripting')
288
+ .option('--json', 'Output in JSON format for scripting')
259
289
  .option('--verbose', 'Enable debug logging')
260
290
  .option('--profile <name>', 'OAuth profile for the server ("default" if not provided)')
261
- .option('--schema <file>', 'Validate tool/prompt schema against expected schema')
262
- .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
263
291
  .option('--timeout <seconds>', 'Request timeout in seconds (default: 300)')
292
+ .option('--max-chars <n>', 'Truncate output to n characters (ignored in --json mode)')
264
293
  .option('--insecure', 'Skip TLS certificate verification (for self-signed certs)')
265
294
  .version(mcpcVersion, '-v, --version', 'Output the version number')
266
295
  .helpOption('-h, --help', 'Display help');
267
296
  program.addHelpText('after', `
268
297
  ${chalk.bold('MCP session commands (after connecting):')}
269
- <@session> Show MCP server info, capabilities, and tools
270
- <@session> ${chalk.cyan('grep')} <pattern> Search tools and instructions
271
- <@session> ${chalk.cyan('tools-list')} List all server tools
272
- <@session> ${chalk.cyan('tools-get')} <name> Get tool details and schema
273
- <@session> ${chalk.cyan('tools-call')} <name> [arg:=val ... | <json> | <stdin]
274
- <@session> ${chalk.cyan('prompts-list')}
275
- <@session> ${chalk.cyan('prompts-get')} <name> [arg:=val ... | <json> | <stdin]
276
- <@session> ${chalk.cyan('resources-list')}
277
- <@session> ${chalk.cyan('resources-read')} <uri>
278
- <@session> ${chalk.cyan('resources-subscribe')} <uri>
279
- <@session> ${chalk.cyan('resources-unsubscribe')} <uri>
280
- <@session> ${chalk.cyan('resources-templates-list')}
281
- <@session> ${chalk.cyan('tasks-list')}
282
- <@session> ${chalk.cyan('tasks-get')} <taskId>
283
- <@session> ${chalk.cyan('tasks-cancel')} <taskId>
284
- <@session> ${chalk.cyan('logging-set-level')} <level>
285
- <@session> ${chalk.cyan('ping')}
298
+ <@session> Show MCP server info, capabilities, and tools overview
299
+ <@session> ${theme.cyan('grep')} <pattern> Search tools and instructions
300
+ <@session> ${theme.cyan('tools-list')} List all server tools
301
+ <@session> ${theme.cyan('tools-get')} <name> Get tool details and schema
302
+ <@session> ${theme.cyan('tools-call')} <name> [arg:=val ... | <json> | <stdin]
303
+ <@session> ${theme.cyan('prompts-list')}
304
+ <@session> ${theme.cyan('prompts-get')} <name> [arg:=val ... | <json> | <stdin]
305
+ <@session> ${theme.cyan('resources-list')}
306
+ <@session> ${theme.cyan('resources-read')} <uri>
307
+ <@session> ${theme.cyan('resources-subscribe')} <uri>
308
+ <@session> ${theme.cyan('resources-unsubscribe')} <uri>
309
+ <@session> ${theme.cyan('resources-templates-list')}
310
+ <@session> ${theme.cyan('tasks-list')}
311
+ <@session> ${theme.cyan('tasks-get')} <taskId>
312
+ <@session> ${theme.cyan('tasks-result')} <taskId>
313
+ <@session> ${theme.cyan('tasks-cancel')} <taskId>
314
+ <@session> ${theme.cyan('logging-set-level')} <level>
315
+ <@session> ${theme.cyan('ping')}
286
316
 
287
317
  Run "mcpc" without arguments to show active sessions and OAuth profiles.
318
+ Run "mcpc --json" to get the same data as \`{ sessions: [...], profiles: [...] }\`.
288
319
 
289
320
  Full docs: ${docsUrl}`);
290
321
  program
291
322
  .command('connect [server] [@session]')
292
- .usage('<server> <@session>')
323
+ .usage('<server> [@session]')
293
324
  .description('Connect to an MCP server and start a new named @session')
294
325
  .option('-H, --header <header>', 'HTTP header (can be repeated)')
295
326
  .option('--profile <name>', 'OAuth profile to use ("default" if skipped)')
296
327
  .option('--no-profile', 'Skip OAuth profile (connect anonymously)')
297
328
  .option('--proxy <[host:]port>', 'Start proxy MCP server for session')
298
329
  .option('--proxy-bearer-token <token>', 'Require authentication for access to proxy server')
330
+ .option('--stdio', 'Launch all local stdio servers from selected config files')
299
331
  .option('--x402', 'Enable x402 auto-payment using the configured wallet')
300
332
  .addHelpText('after', `
301
333
  ${chalk.bold('Server formats:')}
302
- mcp.apify.com Remote HTTP server (https:// added automatically)
334
+ mcp.apify.com Remote HTTP server (https:// auto-added)
303
335
  ~/.vscode/mcp.json:puppeteer Config file entry (file:entry)
304
- `)
336
+ ~/.vscode/mcp.json Config file — connect every entry
337
+ ${chalk.dim('(no server)')} Auto-discover configs and connect everything
338
+
339
+ ${chalk.bold('Auto-discovery (no server arg):')}
340
+ Scans ./ and ~ for .mcp.json, mcp.json, mcp_config.json, .cursor/mcp.json,
341
+ .vscode/mcp.json, .kiro/settings/mcp.json, ~/.claude.json,
342
+ ~/.codeium/windsurf/mcp_config.json, plus VS Code & Claude Desktop configs.
343
+ Set APIFY_API_TOKEN to auto-connect mcp.apify.com as @apify.
344
+
345
+ ${chalk.bold('Session name:')}
346
+ Omit @session to auto-generate from the server (mcp.apify.com → @apify)
347
+ or config entry. Matching sessions (same server, profile, header keys)
348
+ are reused. Bulk connects don't accept @session.
349
+
350
+ ${chalk.bold('Stdio servers (command-based, run locally):')}
351
+ Config entries spawn the command on connect, even if the handshake
352
+ later fails — only connect to configs you trust. Stderr is logged to
353
+ ~/.mcpc/logs/bridge-<session>.log. Bulk connects skip stdio by default;
354
+ pass --stdio to include them.
355
+ ${jsonHelp('Array of `InitializeResult` objects (one per session), extended with `toolNames` and `_mcpc` metadata', '`[{ protocolVersion?, capabilities?, serverInfo?, instructions?, toolNames?, _mcpc: { sessionName, server?, ... }]`', `${SCHEMA_BASE}#initializeresult`)}`)
305
356
  .action(async (server, sessionName, opts, command) => {
306
- if (!server) {
307
- throw new ClientError('Missing required argument: server\n\nExample: mcpc connect mcp.apify.com @myapp');
308
- }
309
- if (!sessionName) {
310
- throw new ClientError('Missing required argument: @session\n\nExample: mcpc connect mcp.apify.com @myapp');
311
- }
312
357
  const globalOpts = getOptionsFromCommand(command);
313
- const parsed = parseServerArg(server);
314
358
  const headers = opts.header
315
359
  ? Array.isArray(opts.header)
316
360
  ? opts.header
317
361
  : [opts.header]
318
362
  : undefined;
363
+ if (!server) {
364
+ if (sessionName) {
365
+ throw new ClientError(`Cannot specify @session name when discovering and connecting all servers.\n` +
366
+ `To connect a specific server, pass a URL or config entry: mcpc connect <server> ${sessionName}`);
367
+ }
368
+ await sessions.connectAllFromStandardConfigs({
369
+ ...globalOpts,
370
+ ...(headers && { headers }),
371
+ ...(opts.proxy && { proxy: opts.proxy }),
372
+ ...(opts.proxyBearerToken && { proxyBearerToken: opts.proxyBearerToken }),
373
+ ...(opts.stdio && { stdio: true }),
374
+ ...(opts.x402 && { x402: opts.x402 }),
375
+ ...(globalOpts.insecure && { insecure: true }),
376
+ });
377
+ return;
378
+ }
379
+ const parsed = parseServerArg(server);
319
380
  if (!parsed) {
320
381
  throw new ClientError(`Invalid server: "${server}"\n\n` +
321
382
  `Expected a URL (e.g. mcp.apify.com) or a config file entry (e.g. ~/.vscode/mcp.json:filesystem)`);
322
383
  }
384
+ if (parsed.type === 'config-file') {
385
+ if (sessionName) {
386
+ throw new ClientError(`Cannot specify @session name when connecting all servers from a config file.\n` +
387
+ `To connect a specific entry, use: mcpc connect ${server}:<entry> ${sessionName}`);
388
+ }
389
+ await sessions.connectAllFromConfig(parsed.file, {
390
+ ...globalOpts,
391
+ ...(headers && { headers }),
392
+ ...(opts.proxy && { proxy: opts.proxy }),
393
+ ...(opts.proxyBearerToken && { proxyBearerToken: opts.proxyBearerToken }),
394
+ ...(opts.stdio && { stdio: true }),
395
+ ...(opts.x402 && { x402: opts.x402 }),
396
+ ...(globalOpts.insecure && { insecure: true }),
397
+ });
398
+ return;
399
+ }
400
+ if (!sessionName) {
401
+ sessionName = await sessions.resolveSessionName(parsed, {
402
+ outputMode: globalOpts.outputMode,
403
+ ...(globalOpts.profile && { profile: globalOpts.profile }),
404
+ ...(headers && { headers }),
405
+ ...(globalOpts.noProfile && { noProfile: globalOpts.noProfile }),
406
+ });
407
+ }
323
408
  if (parsed.type === 'config') {
324
409
  await sessions.connectSession(parsed.entry, sessionName, {
325
410
  ...globalOpts,
@@ -346,6 +431,7 @@ ${chalk.bold('Server formats:')}
346
431
  .command('close [@session]')
347
432
  .usage('<@session>')
348
433
  .description('Close a session')
434
+ .addHelpText('after', jsonHelp('`{ sessionName, closed: true }`'))
349
435
  .action(async (sessionName, _opts, command) => {
350
436
  if (!sessionName) {
351
437
  throw new ClientError('Missing required argument: @session\n\nExample: mcpc close @myapp');
@@ -377,18 +463,48 @@ ${chalk.bold('Server formats:')}
377
463
  .usage('<server>')
378
464
  .description('Interactively login to a server using OAuth and save profile')
379
465
  .option('--profile <name>', 'Profile name (default: "default")')
380
- .option('--scope <scopes>', 'OAuth scopes to request, quoted and space-separated (e.g. --scope "read write")')
381
- .option('--client-id <id>', 'OAuth client ID (for servers without dynamic client registration)')
382
- .option('--client-secret <secret>', 'OAuth client secret (for servers without dynamic client registration)')
466
+ .option('--scope <scopes>', 'OAuth scopes to request (e.g. --scope "read write")')
467
+ .option('--client-id <id>', 'Pre-registered OAuth client ID (skips CIMD and DCR)')
468
+ .option('--client-secret <secret>', 'Pre-registered OAuth client secret (requires --client-id)')
469
+ .option('--client-metadata-url <url>', 'HTTPS URL of an OAuth CIMD (default: https://apify.github.io/mcpc/client-metadata.json)')
470
+ .option('--no-client-metadata-url', 'Disable CIMD; force DCR on CIMD-capable servers')
471
+ .option('--callback-port <port>', 'Loopback port for OAuth callback (default: 13316–13325)')
472
+ .addHelpText('after', `
473
+ ${chalk.bold('OAuth client registration approaches:')}
474
+
475
+ 1. Pre-registration: --client-id (and optionally --client-secret).
476
+ 2. Client ID Metadata Documents (CIMD): used by default. mcpc ships with a
477
+ hosted CIMD at https://apify.github.io/mcpc/client-metadata.json
478
+ which identifies all mcpc installs as the same client. Override with
479
+ --client-metadata-url <url> or disable with --no-client-metadata-url.
480
+ Active only when the authorization server advertises
481
+ "client_id_metadata_document_supported: true".
482
+ 3. Dynamic Client Registration (DCR): fallback when the server exposes a
483
+ "registration_endpoint" and CIMD is not supported or disabled.
484
+
485
+ See https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
486
+
487
+ ${jsonHelp('Interactive prompts are written to stderr, stdout contains a clean JSON object', '`{ profile, serverUrl, scopes }`')}
488
+ `)
383
489
  .action(async (server, opts, command) => {
384
490
  if (!server) {
385
491
  throw new ClientError('Missing required argument: server\n\nExample: mcpc login mcp.apify.com');
386
492
  }
493
+ let callbackPort;
494
+ if (opts.callbackPort) {
495
+ const parsed = parseInt(opts.callbackPort, 10);
496
+ if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
497
+ throw new ClientError(`Invalid --callback-port value: "${opts.callbackPort}". Must be an integer between 1 and 65535.`);
498
+ }
499
+ callbackPort = parsed;
500
+ }
387
501
  await auth.login(server, {
388
502
  profile: opts.profile,
389
503
  scope: opts.scope,
390
504
  clientId: opts.clientId,
391
505
  clientSecret: opts.clientSecret,
506
+ clientMetadataUrl: opts.clientMetadataUrl,
507
+ ...(callbackPort !== undefined ? { callbackPort } : {}),
392
508
  ...getOptionsFromCommand(command),
393
509
  });
394
510
  });
@@ -397,6 +513,7 @@ ${chalk.bold('Server formats:')}
397
513
  .usage('<server>')
398
514
  .description('Delete an OAuth profile for a server')
399
515
  .option('--profile <name>', 'Profile name (default: "default")')
516
+ .addHelpText('after', jsonHelp('`{ profile, serverUrl, deleted: true, affectedSessions }`'))
400
517
  .action(async (server, opts, command) => {
401
518
  if (!server) {
402
519
  throw new ClientError('Missing required argument: server\n\nExample: mcpc logout mcp.apify.com');
@@ -417,7 +534,7 @@ ${chalk.bold('Resources:')}
417
534
  all Remove all of the above
418
535
 
419
536
  Without arguments, performs safe cleanup of stale data only.
420
- `)
537
+ ${jsonHelp('`{ crashedBridges, expiredSessions, orphanedBridgeLogs, sessions, profiles, logs }`')}`)
421
538
  .action(async (resources, _opts, command) => {
422
539
  const globalOpts = getOptionsFromCommand(command);
423
540
  const VALID_CLEAN_TYPES = ['sessions', 'profiles', 'logs', 'all'];
@@ -458,7 +575,7 @@ ${chalk.bold('Examples:')}
458
575
  mcpc @apify grep "actor" Search within a single session
459
576
  mcpc grep "file" --json JSON output for scripting
460
577
  mcpc grep "actor" -m 5 Show at most 5 results
461
- `)
578
+ ${jsonHelp('`[{ sessionName, tools?: Tool[], resources?: Resource[], prompts?: Prompt[], instructions?: string[] }]`')}`)
462
579
  .action(async (pattern, opts, command) => {
463
580
  if (!pattern) {
464
581
  throw new ClientError('Missing required argument: pattern\n\nUsage: mcpc grep <pattern>\n\nExample: mcpc grep "search"');
@@ -496,18 +613,17 @@ ${chalk.bold('Examples:')}
496
613
  }
497
614
  const topLevelCmd = program.commands.find((c) => c.name() === cmdName || c.aliases().includes(cmdName));
498
615
  if (topLevelCmd) {
616
+ tuneCommandHelp(topLevelCmd);
499
617
  topLevelCmd.outputHelp();
500
618
  return;
501
619
  }
502
- const dummyProgram = new Command();
503
- dummyProgram.name('mcpc <@session>');
504
- registerSessionCommands(dummyProgram, '@dummy');
505
- const sessionCmd = dummyProgram.commands.find((c) => c.name() === cmdName || c.aliases().includes(cmdName));
506
- if (sessionCmd) {
507
- sessionCmd.outputHelp();
620
+ if (showSessionCommandHelp(cmdName))
508
621
  return;
509
- }
510
622
  console.error(`Unknown command: ${cmdName}`);
623
+ const suggestion = suggestCommand(cmdName, [...KNOWN_COMMANDS, ...KNOWN_SESSION_COMMANDS]);
624
+ if (suggestion) {
625
+ console.error(`\nDid you mean: mcpc help ${suggestion}`);
626
+ }
511
627
  console.error(`Run "mcpc --help" for usage information.`);
512
628
  process.exit(1);
513
629
  });
@@ -523,57 +639,158 @@ ${chalk.bold('Examples:')}
523
639
  });
524
640
  return program;
525
641
  }
642
+ const NO_JSON_COMMANDS = new Set(['shell']);
643
+ function tuneCommandHelp(cmd) {
644
+ if (!NO_JSON_COMMANDS.has(cmd.name()) && !cmd.options.some((o) => o.long === '--json')) {
645
+ cmd.option('--json', 'Output in JSON format');
646
+ }
647
+ cmd.helpOption('-h, --help', 'Display help');
648
+ const helpOpt = cmd._getHelpOption?.();
649
+ if (helpOpt)
650
+ helpOpt.hidden = true;
651
+ }
652
+ function showSessionCommandHelp(cmdName) {
653
+ const dummyProgram = createSessionProgram();
654
+ registerSessionCommands(dummyProgram, '<@session>');
655
+ for (const cmd of dummyProgram.commands) {
656
+ tuneCommandHelp(cmd);
657
+ }
658
+ const sessionCmd = dummyProgram.commands.find((c) => c.name() === cmdName || c.aliases().includes(cmdName));
659
+ if (sessionCmd) {
660
+ sessionCmd.outputHelp();
661
+ return true;
662
+ }
663
+ return false;
664
+ }
526
665
  function registerSessionCommands(program, session) {
527
666
  program
528
- .command('help')
529
- .description('Show server instructions and available capabilities')
530
- .action(async (_options, command) => {
531
- await sessions.showHelp(session, getOptionsFromCommand(command));
667
+ .command('help', { hidden: true })
668
+ .description('Show available commands and options.')
669
+ .action((_options, command) => {
670
+ command.parent.outputHelp();
532
671
  });
533
672
  program
534
673
  .command('shell')
535
- .description('Interactive shell for the session')
674
+ .description('Launch interactive MCP shell.')
536
675
  .action(async () => {
537
676
  await sessions.openShell(session);
538
677
  });
539
678
  program
540
- .command('close', { hidden: true })
541
- .description('Close the session')
679
+ .command('close')
680
+ .description('Close MCP session.')
542
681
  .action(async (_options, command) => {
543
682
  await sessions.closeSession(session, getOptionsFromCommand(command));
544
683
  });
545
684
  program
546
685
  .command('restart')
547
- .description('Restart the session (stop and start the bridge)')
686
+ .description('Restart MCP session (losing all state).')
548
687
  .action(async (_options, command) => {
549
688
  await sessions.restartSession(session, getOptionsFromCommand(command));
550
689
  });
551
690
  program
552
- .command('tools')
553
- .description('List available tools (shorthand for tools-list)')
554
- .option('--full', 'Show full tool details including complete input schema')
555
- .action(async (_options, command) => {
556
- await tools.listTools(session, getOptionsFromCommand(command));
691
+ .command('grep <pattern>')
692
+ .usage('<pattern> [options]')
693
+ .description('Search MCP session objects.')
694
+ .option('--tools', 'Search tools')
695
+ .option('--resources', 'Search resources')
696
+ .option('--prompts', 'Search prompts')
697
+ .option('--instructions', 'Search server instructions')
698
+ .option('-E, --regex', 'Treat pattern as a regular expression')
699
+ .option('-s, --case-sensitive', 'Case-sensitive matching')
700
+ .option('-m, --max-results <n>', 'Limit the number of results')
701
+ .addHelpText('after', `
702
+ ${chalk.bold('Type filters:')}
703
+ By default, tools and instructions are searched. Use --resources or --prompts
704
+ to search those instead. Combine flags to search multiple types.
705
+
706
+ ${chalk.bold('Examples:')}
707
+ mcpc ${session} grep "search" Search tools and instructions
708
+ mcpc ${session} grep "search" --resources Search resources only
709
+ mcpc ${session} grep "search|find" -E Regex search
710
+ ${jsonHelp('`{ tools?: Tool[], resources?: Resource[], prompts?: Prompt[], instructions?: string[] }`')}`)
711
+ .action(async (pattern, opts, command) => {
712
+ const globalOpts = getOptionsFromCommand(command);
713
+ const maxResults = opts.maxResults ? parseInt(opts.maxResults, 10) : undefined;
714
+ const exitCode = await grepCmd.grepSession(session, pattern, {
715
+ tools: opts.tools,
716
+ resources: opts.resources,
717
+ prompts: opts.prompts,
718
+ instructions: opts.instructions,
719
+ regex: opts.regex,
720
+ caseSensitive: opts.caseSensitive,
721
+ maxResults,
722
+ ...globalOpts,
723
+ });
724
+ process.exit(exitCode);
557
725
  });
558
726
  program
559
727
  .command('tools-list')
560
- .description('List available tools')
561
- .option('--full', 'Show full tool details including complete input schema')
728
+ .description('List all MCP tools.')
729
+ .option('--full', 'Show full tool details including schema')
730
+ .addHelpText('after', jsonHelp('Array of `Tool` objects', '`[{ name, description?, inputSchema, outputSchema?, annotations? }, ...]`', `${SCHEMA_BASE}#tool`))
562
731
  .action(async (_options, command) => {
563
732
  await tools.listTools(session, getOptionsFromCommand(command));
564
733
  });
565
734
  program
566
735
  .command('tools-get <name>')
567
- .description('Get information about a specific tool')
736
+ .description('Get details and schema for an MCP tool.')
737
+ .option('--schema <file>', 'Validate tool schema against expected schema')
738
+ .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
739
+ .addHelpText('after', `
740
+ ${chalk.bold('Schema validation:')}
741
+ --schema <file> Validate against expected schema (save with tools-get --json)
742
+ --schema-mode <mode> strict | compatible (default) | ignore
743
+ ${jsonHelp('`Tool` object', '`{ name, description?, inputSchema, outputSchema?, annotations? }`', `${SCHEMA_BASE}#tool`)}`)
568
744
  .action(async (name, _options, command) => {
569
745
  await tools.getTool(session, name, getOptionsFromCommand(command));
570
746
  });
747
+ const toolsCallJsonHelp = jsonHelp('`CallToolResult` object', '`{ content: [{ type, text?, ... }], isError?, structuredContent?: { ... } }`', `${SCHEMA_BASE}#calltoolresult`);
748
+ const toolsCallCombinedJsonHelp = `
749
+ ${chalk.bold('JSON output (--json):')}
750
+ \`CallToolResult\` object:
751
+ \`{ content: [{ type, text?, ... }], isError?, structuredContent?: { ... } }\`
752
+ Schema: ${SCHEMA_BASE}#calltoolresult
753
+
754
+ With \`--detach\`: \`CreateTaskResult\` object:
755
+ \`{ taskId: string, status: string }\`
756
+ Schema: ${SCHEMA_BASE}#createtaskresult
757
+ `;
571
758
  program
572
759
  .command('tools-call <name> [args...]')
573
- .description('Call a tool with arguments (key:=value pairs or JSON)')
574
- .option('--task', 'Use task execution (experimental)')
760
+ .description('Call an MCP tool with arguments.')
761
+ .helpOption(false)
762
+ .option('--task', 'Use async task execution; Ctrl+C prints the task ID and exits (experimental)')
575
763
  .option('--detach', 'Start task and return immediately with task ID (implies --task)')
764
+ .option('--schema <file>', 'Validate tool schema against expected schema before calling')
765
+ .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
766
+ .addHelpText('after', `
767
+ ${chalk.bold('Arguments:')}
768
+ key:=value pairs mcpc ${session} tools-call search query:=hello limit:=10
769
+ Inline JSON mcpc ${session} tools-call search '{"query":"hello"}'
770
+ Stdin pipe echo '{"query":"hello"}' | mcpc ${session} tools-call search
771
+
772
+ Values are auto-parsed: strings, numbers, booleans, JSON objects/arrays.
773
+ To force a string, wrap in quotes: id:='"123"'
774
+
775
+ ${chalk.bold('Async tasks (--task, --detach):')}
776
+ --task shows a progress spinner while the task runs on the server.
777
+ If you press Ctrl+C, the task keeps running and a hint with the task ID
778
+ is printed so you can fetch or cancel it later.
779
+ --detach returns the task ID immediately without waiting.
780
+
781
+ ${chalk.bold('Schema validation:')}
782
+ --schema <file> Validate tool schema before calling (save with tools-get --json)
783
+ --schema-mode <mode> strict | compatible (default) | ignore
784
+ ${toolsCallCombinedJsonHelp}`)
576
785
  .action(async (name, args, options, command) => {
786
+ if (name === '--help' || name === '-h') {
787
+ command.help();
788
+ return;
789
+ }
790
+ if (args.includes('--help') || args.includes('-h')) {
791
+ await tools.getTool(session, name, getOptionsFromCommand(command));
792
+ return;
793
+ }
577
794
  await tools.callTool(session, name, {
578
795
  args,
579
796
  task: options.task,
@@ -583,39 +800,45 @@ function registerSessionCommands(program, session) {
583
800
  });
584
801
  program
585
802
  .command('tasks-list')
586
- .description('List active tasks')
803
+ .description('List all MCP tasks.')
804
+ .addHelpText('after', jsonHelp('`{ tasks: Task[] }`', '`{ tasks: [{ taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage?, pollInterval? }] }`', `${SCHEMA_BASE}#task`))
587
805
  .action(async (_options, command) => {
588
806
  await tasks.listTasks(session, getOptionsFromCommand(command));
589
807
  });
590
808
  program
591
809
  .command('tasks-get <taskId>')
592
- .description('Get status of a specific task')
810
+ .description('Get MCP task status.')
811
+ .addHelpText('after', jsonHelp('`Task` object', '`{ taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage?, pollInterval? }`', `${SCHEMA_BASE}#task`))
593
812
  .action(async (taskId, _options, command) => {
594
813
  await tasks.getTask(session, taskId, getOptionsFromCommand(command));
595
814
  });
596
815
  program
597
- .command('tasks-cancel <taskId>')
598
- .description('Cancel a running task')
816
+ .command('tasks-result <taskId>')
817
+ .description('Get MCP task final result (blocks until task reaches a terminal state).')
818
+ .addHelpText('after', toolsCallJsonHelp)
599
819
  .action(async (taskId, _options, command) => {
600
- await tasks.cancelTask(session, taskId, getOptionsFromCommand(command));
820
+ await tasks.getTaskResult(session, taskId, getOptionsFromCommand(command));
601
821
  });
602
822
  program
603
- .command('resources')
604
- .description('List available resources (shorthand for resources-list)')
605
- .action(async (_options, command) => {
606
- await resources.listResources(session, getOptionsFromCommand(command));
823
+ .command('tasks-cancel <taskId>')
824
+ .description('Cancel an MCP task.')
825
+ .addHelpText('after', jsonHelp('`Task` object', '`{ taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage?, pollInterval? }`', `${SCHEMA_BASE}#task`))
826
+ .action(async (taskId, _options, command) => {
827
+ await tasks.cancelTask(session, taskId, getOptionsFromCommand(command));
607
828
  });
608
829
  program
609
830
  .command('resources-list')
610
- .description('List available resources')
831
+ .description('List all MCP resources.')
832
+ .addHelpText('after', jsonHelp('Array of `Resource` objects', '`[{ uri, name?, description?, mimeType? }, ...]`', `${SCHEMA_BASE}#resource`))
611
833
  .action(async (_options, command) => {
612
834
  await resources.listResources(session, getOptionsFromCommand(command));
613
835
  });
614
836
  program
615
837
  .command('resources-read <uri>')
616
- .description('Get a resource by URI')
838
+ .description('Read an MCP resource by URI.')
617
839
  .option('-o, --output <file>', 'Write resource to file')
618
840
  .option('--max-size <bytes>', 'Maximum resource size in bytes')
841
+ .addHelpText('after', jsonHelp('`ReadResourceResult` object', '`{ contents: [{ uri, mimeType?, text? | blob? }] }`', `${SCHEMA_BASE}#readresourceresult`))
619
842
  .action(async (uri, options, command) => {
620
843
  await resources.getResource(session, uri, {
621
844
  output: options.output,
@@ -625,37 +848,44 @@ function registerSessionCommands(program, session) {
625
848
  });
626
849
  program
627
850
  .command('resources-subscribe <uri>')
628
- .description('Subscribe to resource updates')
851
+ .description('Subscribe to MCP resource updates.')
852
+ .addHelpText('after', jsonHelp('`{ subscribed: true, uri: string }`'))
629
853
  .action(async (uri, _options, command) => {
630
854
  await resources.subscribeResource(session, uri, getOptionsFromCommand(command));
631
855
  });
632
856
  program
633
857
  .command('resources-unsubscribe <uri>')
634
- .description('Unsubscribe from resource updates')
858
+ .description('Unsubscribe from MCP resource updates.')
859
+ .addHelpText('after', jsonHelp('`{ unsubscribed: true, uri: string }`'))
635
860
  .action(async (uri, _options, command) => {
636
861
  await resources.unsubscribeResource(session, uri, getOptionsFromCommand(command));
637
862
  });
638
863
  program
639
864
  .command('resources-templates-list')
640
- .description('List available resource templates')
865
+ .description('List MCP resource templates.')
866
+ .addHelpText('after', jsonHelp('Array of `ResourceTemplate` objects', '`[{ uriTemplate, name?, description?, mimeType? }, ...]`', `${SCHEMA_BASE}#resourcetemplate`))
641
867
  .action(async (_options, command) => {
642
868
  await resources.listResourceTemplates(session, getOptionsFromCommand(command));
643
869
  });
644
- program
645
- .command('prompts')
646
- .description('List available prompts (shorthand for prompts-list)')
647
- .action(async (_options, command) => {
648
- await prompts.listPrompts(session, getOptionsFromCommand(command));
649
- });
650
870
  program
651
871
  .command('prompts-list')
652
- .description('List available prompts')
872
+ .description('List all MCP prompts.')
873
+ .addHelpText('after', jsonHelp('Array of `Prompt` objects', '`[{ name, description?, arguments?: [{ name, required? }] }, ...]`', `${SCHEMA_BASE}#prompt`))
653
874
  .action(async (_options, command) => {
654
875
  await prompts.listPrompts(session, getOptionsFromCommand(command));
655
876
  });
656
877
  program
657
878
  .command('prompts-get <name> [args...]')
658
- .description('Get a prompt by name with arguments (key:=value pairs or JSON)')
879
+ .description('Get an MCP prompt with arguments.')
880
+ .addHelpText('after', `
881
+ ${chalk.bold('Arguments:')}
882
+ key:=value pairs mcpc ${session} prompts-get summarize style:=brief lang:=en
883
+ Inline JSON mcpc ${session} prompts-get summarize '{"style":"brief"}'
884
+ Stdin pipe echo '{"style":"brief"}' | mcpc ${session} prompts-get summarize
885
+
886
+ Values are auto-parsed: strings, numbers, booleans, JSON objects/arrays.
887
+ To force a string, wrap in quotes: id:='"123"'
888
+ ${jsonHelp('`GetPromptResult` object', '`{ description?, messages: [{ role, content: { type, text?, ... } }] }`', `${SCHEMA_BASE}#getpromptresult`)}`)
659
889
  .action(async (name, args, _options, command) => {
660
890
  await prompts.getPrompt(session, name, {
661
891
  args,
@@ -664,63 +894,47 @@ function registerSessionCommands(program, session) {
664
894
  });
665
895
  program
666
896
  .command('logging-set-level <level>')
667
- .description('Set server logging level (debug, info, notice, warning, error, critical, alert, emergency)')
897
+ .description('Set MCP server logging level.')
898
+ .addHelpText('after', jsonHelp('`{ level: string }`'))
668
899
  .action(async (level, _options, command) => {
669
900
  await logging.setLogLevel(session, level, getOptionsFromCommand(command));
670
901
  });
671
902
  program
672
903
  .command('ping')
673
- .description('Ping the MCP server to check if it is alive')
904
+ .description('Ping the MCP server.')
905
+ .addHelpText('after', jsonHelp('`{ success: true, durationMs: number }`'))
674
906
  .action(async (_options, command) => {
675
907
  await utilities.ping(session, getOptionsFromCommand(command));
676
908
  });
677
- program
678
- .command('grep <pattern>')
679
- .description('Search tools and instructions')
680
- .option('--tools', 'Search tools')
681
- .option('--resources', 'Search resources')
682
- .option('--prompts', 'Search prompts')
683
- .option('--instructions', 'Search server instructions')
684
- .option('-E, --regex', 'Treat pattern as a regular expression')
685
- .option('-s, --case-sensitive', 'Case-sensitive matching')
686
- .option('-m, --max-results <n>', 'Limit the number of results')
687
- .action(async (pattern, opts, command) => {
688
- const globalOpts = getOptionsFromCommand(command);
689
- const maxResults = opts.maxResults ? parseInt(opts.maxResults, 10) : undefined;
690
- const exitCode = await grepCmd.grepSession(session, pattern, {
691
- tools: opts.tools,
692
- resources: opts.resources,
693
- prompts: opts.prompts,
694
- instructions: opts.instructions,
695
- regex: opts.regex,
696
- caseSensitive: opts.caseSensitive,
697
- maxResults,
698
- ...globalOpts,
699
- });
700
- process.exit(exitCode);
701
- });
702
909
  }
703
910
  function createSessionProgram() {
704
911
  const program = new Command();
705
912
  program.configureOutput({
706
- outputError: (str, write) => write(str),
913
+ outputError: () => { },
707
914
  getOutHelpWidth: () => 100,
708
915
  getErrHelpWidth: () => 100,
709
916
  });
917
+ program.configureHelp({
918
+ subcommandTerm: (cmd) => `${cmd.name()} ${cmd.usage()}`.replace(/^\[options\]\s*|\s*\[options\]/g, '').trim(),
919
+ styleTitle: (str) => chalk.bold(str),
920
+ styleSubcommandText: (str) => theme.cyan(str),
921
+ });
710
922
  program
711
923
  .name('mcpc <@session>')
924
+ .description('Execute MCP commands on a connected session.')
712
925
  .helpOption('-h, --help', 'Display help')
713
- .option('-j, --json', 'Output in JSON format for scripting and code mode')
926
+ .option('--json', 'Output in JSON format for scripting and code mode')
714
927
  .option('--verbose', 'Enable debug logging')
715
928
  .option('--profile <name>', 'OAuth profile override')
716
- .option('--schema <file>', 'Validate tool/prompt schema against expected schema')
717
- .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
718
929
  .option('--timeout <seconds>', 'Request timeout in seconds (default: 300)')
719
- .option('--insecure', 'Skip TLS certificate verification (for self-signed certs)');
930
+ .option('--max-chars <n>', 'Truncate output to n characters (ignored in --json mode)')
931
+ .option('--insecure', 'Skip TLS certificate verification (for self-signed certs)')
932
+ .addHelpText('after', `\nWhen no command is given, shows server info, capabilities, and tools.\n`);
720
933
  return program;
721
934
  }
722
935
  async function handleSessionCommands(session, args) {
723
- if (!hasSubcommand(args)) {
936
+ const argsSlice = args.slice(2);
937
+ if (!hasSubcommand(args) && !argsSlice.includes('--help') && !argsSlice.includes('-h')) {
724
938
  const options = extractOptions(args);
725
939
  if (options.verbose)
726
940
  setVerbose(true);
@@ -734,25 +948,50 @@ async function handleSessionCommands(session, args) {
734
948
  return;
735
949
  }
736
950
  const program = createSessionProgram();
951
+ program.name(`mcpc ${session}`);
952
+ program.exitOverride();
737
953
  registerSessionCommands(program, session);
954
+ for (const cmd of program.commands) {
955
+ tuneCommandHelp(cmd);
956
+ }
738
957
  try {
739
958
  await program.parseAsync(args);
740
959
  }
741
960
  catch (error) {
742
961
  const opts = program.opts();
743
962
  const outputMode = opts.json ? 'json' : 'human';
963
+ if (error instanceof CommanderError && error.code === 'commander.unknownCommand') {
964
+ const unknownCmd = args.find((a, i) => i >= 2 && !a.startsWith('-') && !KNOWN_SESSION_COMMANDS.includes(a));
965
+ if (unknownCmd) {
966
+ const suggestion = suggestCommand(unknownCmd, KNOWN_SESSION_COMMANDS);
967
+ if (outputMode === 'json') {
968
+ console.error(formatJsonError(new Error(`Unknown command: ${unknownCmd}`), 1));
969
+ }
970
+ else {
971
+ console.error(`Error: Unknown command: ${unknownCmd}`);
972
+ if (suggestion) {
973
+ console.error(`\nDid you mean: mcpc ${session} ${suggestion}`);
974
+ }
975
+ console.error(`Run "mcpc ${session} --help" for available commands.\n`);
976
+ }
977
+ process.exit(1);
978
+ }
979
+ }
980
+ if (error instanceof CommanderError && error.code === 'commander.helpDisplayed') {
981
+ process.exit(0);
982
+ }
744
983
  if (isMcpError(error)) {
745
984
  if (outputMode === 'json') {
746
985
  console.error(formatJsonError(error, error.code));
747
986
  }
748
987
  else {
749
- console.error(chalk.red(formatHumanError(error, opts.verbose)));
988
+ console.error(theme.red(formatHumanError(error, opts.verbose)));
750
989
  }
751
990
  process.exit(error.code);
752
991
  }
753
992
  console.error(outputMode === 'json'
754
993
  ? formatJsonError(error, 1)
755
- : chalk.red(formatHumanError(error, opts.verbose)));
994
+ : theme.red(formatHumanError(error, opts.verbose)));
756
995
  process.exit(1);
757
996
  }
758
997
  }