@apify/mcpc 0.2.3 → 0.2.6

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 (94) hide show
  1. package/CHANGELOG.md +58 -2
  2. package/README.md +87 -35
  3. package/dist/bridge/index.js +63 -5
  4. package/dist/bridge/index.js.map +1 -1
  5. package/dist/cli/commands/auth.d.ts +1 -0
  6. package/dist/cli/commands/auth.d.ts.map +1 -1
  7. package/dist/cli/commands/auth.js +7 -0
  8. package/dist/cli/commands/auth.js.map +1 -1
  9. package/dist/cli/commands/clean.d.ts.map +1 -1
  10. package/dist/cli/commands/clean.js +13 -2
  11. package/dist/cli/commands/clean.js.map +1 -1
  12. package/dist/cli/commands/grep.d.ts +4 -0
  13. package/dist/cli/commands/grep.d.ts.map +1 -1
  14. package/dist/cli/commands/grep.js +121 -11
  15. package/dist/cli/commands/grep.js.map +1 -1
  16. package/dist/cli/commands/prompts.d.ts.map +1 -1
  17. package/dist/cli/commands/prompts.js +7 -26
  18. package/dist/cli/commands/prompts.js.map +1 -1
  19. package/dist/cli/commands/resources.d.ts.map +1 -1
  20. package/dist/cli/commands/resources.js +9 -3
  21. package/dist/cli/commands/resources.js.map +1 -1
  22. package/dist/cli/commands/sessions.d.ts +26 -2
  23. package/dist/cli/commands/sessions.d.ts.map +1 -1
  24. package/dist/cli/commands/sessions.js +191 -16
  25. package/dist/cli/commands/sessions.js.map +1 -1
  26. package/dist/cli/commands/tasks.d.ts +1 -0
  27. package/dist/cli/commands/tasks.d.ts.map +1 -1
  28. package/dist/cli/commands/tasks.js +11 -0
  29. package/dist/cli/commands/tasks.js.map +1 -1
  30. package/dist/cli/commands/tools.d.ts +6 -1
  31. package/dist/cli/commands/tools.d.ts.map +1 -1
  32. package/dist/cli/commands/tools.js +43 -15
  33. package/dist/cli/commands/tools.js.map +1 -1
  34. package/dist/cli/commands/x402.d.ts.map +1 -1
  35. package/dist/cli/commands/x402.js +6 -2
  36. package/dist/cli/commands/x402.js.map +1 -1
  37. package/dist/cli/index.js +308 -91
  38. package/dist/cli/index.js.map +1 -1
  39. package/dist/cli/output.d.ts +5 -0
  40. package/dist/cli/output.d.ts.map +1 -1
  41. package/dist/cli/output.js +103 -16
  42. package/dist/cli/output.js.map +1 -1
  43. package/dist/cli/parser.d.ts +4 -0
  44. package/dist/cli/parser.d.ts.map +1 -1
  45. package/dist/cli/parser.js +50 -16
  46. package/dist/cli/parser.js.map +1 -1
  47. package/dist/cli/shell.d.ts.map +1 -1
  48. package/dist/cli/shell.js +27 -4
  49. package/dist/cli/shell.js.map +1 -1
  50. package/dist/core/mcp-client.d.ts +1 -0
  51. package/dist/core/mcp-client.d.ts.map +1 -1
  52. package/dist/core/mcp-client.js +14 -0
  53. package/dist/core/mcp-client.js.map +1 -1
  54. package/dist/lib/auth/oauth-flow.d.ts +1 -0
  55. package/dist/lib/auth/oauth-flow.d.ts.map +1 -1
  56. package/dist/lib/auth/oauth-flow.js +60 -16
  57. package/dist/lib/auth/oauth-flow.js.map +1 -1
  58. package/dist/lib/auth/oauth-provider.d.ts +2 -0
  59. package/dist/lib/auth/oauth-provider.d.ts.map +1 -1
  60. package/dist/lib/auth/oauth-provider.js +4 -0
  61. package/dist/lib/auth/oauth-provider.js.map +1 -1
  62. package/dist/lib/bridge-client.d.ts +1 -0
  63. package/dist/lib/bridge-client.d.ts.map +1 -1
  64. package/dist/lib/bridge-client.js.map +1 -1
  65. package/dist/lib/bridge-manager.d.ts +4 -1
  66. package/dist/lib/bridge-manager.d.ts.map +1 -1
  67. package/dist/lib/bridge-manager.js +97 -27
  68. package/dist/lib/bridge-manager.js.map +1 -1
  69. package/dist/lib/cleanup.d.ts +5 -0
  70. package/dist/lib/cleanup.d.ts.map +1 -1
  71. package/dist/lib/cleanup.js +38 -1
  72. package/dist/lib/cleanup.js.map +1 -1
  73. package/dist/lib/config.d.ts.map +1 -1
  74. package/dist/lib/config.js +5 -1
  75. package/dist/lib/config.js.map +1 -1
  76. package/dist/lib/errors.d.ts.map +1 -1
  77. package/dist/lib/errors.js +15 -8
  78. package/dist/lib/errors.js.map +1 -1
  79. package/dist/lib/session-client.d.ts +1 -0
  80. package/dist/lib/session-client.d.ts.map +1 -1
  81. package/dist/lib/session-client.js +10 -4
  82. package/dist/lib/session-client.js.map +1 -1
  83. package/dist/lib/sessions.d.ts +1 -0
  84. package/dist/lib/sessions.d.ts.map +1 -1
  85. package/dist/lib/sessions.js +52 -4
  86. package/dist/lib/sessions.js.map +1 -1
  87. package/dist/lib/types.d.ts +5 -1
  88. package/dist/lib/types.d.ts.map +1 -1
  89. package/dist/lib/utils.d.ts +16 -3
  90. package/dist/lib/utils.d.ts.map +1 -1
  91. package/dist/lib/utils.js +125 -9
  92. package/dist/lib/utils.js.map +1 -1
  93. package/docs/TODOs.md +11 -0
  94. package/package.json +6 -6
package/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { initProxy } from '../lib/proxy.js';
3
- import { Command } 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';
@@ -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,16 +103,26 @@ 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);
@@ -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
  }
@@ -232,6 +264,21 @@ function createTopLevelProgram() {
232
264
  subcommandTerm: (cmd) => `${cmd.name()} ${cmd.usage()}`.replace(/^\[options\]\s*|\s*\[options\]/g, '').trim(),
233
265
  styleTitle: (str) => chalk.bold(str),
234
266
  styleSubcommandText: (str) => chalk.cyan(str),
267
+ formatHelp: (cmd, helper) => {
268
+ const output = Help.prototype.formatHelp.call(helper, cmd, helper);
269
+ const sections = output.split('\n\n');
270
+ const optIdx = sections.findIndex((s) => s.includes('Options:'));
271
+ const cmdIdx = sections.findIndex((s) => s.includes('Commands:'));
272
+ if (optIdx >= 0 && cmdIdx >= 0 && optIdx < cmdIdx) {
273
+ const tmp = sections[optIdx];
274
+ sections[optIdx] = sections[cmdIdx];
275
+ sections[cmdIdx] = tmp;
276
+ }
277
+ return (sections
278
+ .map((s) => s.trimEnd())
279
+ .filter((s) => s !== '')
280
+ .join('\n\n') + '\n');
281
+ },
235
282
  });
236
283
  const docsUrl = process.stdout.isTTY
237
284
  ? `https://github.com/apify/mcpc/tree/v${mcpcVersion}`
@@ -239,20 +286,19 @@ function createTopLevelProgram() {
239
286
  program
240
287
  .name('mcpc')
241
288
  .description(`${rainbow('Universal')} command-line client for the Model Context Protocol (MCP).`)
242
- .usage('[options] [<@session>] [<command>]')
243
- .option('-j, --json', 'Output in JSON format for scripting')
289
+ .usage('[<@session>] [<command>] [options]')
290
+ .option('--json', 'Output in JSON format for scripting')
244
291
  .option('--verbose', 'Enable debug logging')
245
292
  .option('--profile <name>', 'OAuth profile for the server ("default" if not provided)')
246
- .option('--schema <file>', 'Validate tool/prompt schema against expected schema')
247
- .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
248
293
  .option('--timeout <seconds>', 'Request timeout in seconds (default: 300)')
294
+ .option('--max-chars <n>', 'Truncate output to n characters (ignored in --json mode)')
249
295
  .option('--insecure', 'Skip TLS certificate verification (for self-signed certs)')
250
296
  .version(mcpcVersion, '-v, --version', 'Output the version number')
251
297
  .helpOption('-h, --help', 'Display help');
252
298
  program.addHelpText('after', `
253
299
  ${chalk.bold('MCP session commands (after connecting):')}
254
- <@session> Show MCP server info, capabilities, and tools
255
- <@session> ${chalk.cyan('grep')} <pattern> Search tools, resources, or prompts
300
+ <@session> Show MCP server info, capabilities, and tools overview
301
+ <@session> ${chalk.cyan('grep')} <pattern> Search tools and instructions
256
302
  <@session> ${chalk.cyan('tools-list')} List all server tools
257
303
  <@session> ${chalk.cyan('tools-get')} <name> Get tool details and schema
258
304
  <@session> ${chalk.cyan('tools-call')} <name> [arg:=val ... | <json> | <stdin]
@@ -265,6 +311,7 @@ ${chalk.bold('MCP session commands (after connecting):')}
265
311
  <@session> ${chalk.cyan('resources-templates-list')}
266
312
  <@session> ${chalk.cyan('tasks-list')}
267
313
  <@session> ${chalk.cyan('tasks-get')} <taskId>
314
+ <@session> ${chalk.cyan('tasks-result')} <taskId>
268
315
  <@session> ${chalk.cyan('tasks-cancel')} <taskId>
269
316
  <@session> ${chalk.cyan('logging-set-level')} <level>
270
317
  <@session> ${chalk.cyan('ping')}
@@ -274,8 +321,8 @@ Run "mcpc" without arguments to show active sessions and OAuth profiles.
274
321
  Full docs: ${docsUrl}`);
275
322
  program
276
323
  .command('connect [server] [@session]')
277
- .usage('<server> <@session>')
278
- .description('Connect to an MCP server and start a new named @session')
324
+ .usage('<server> [@session]')
325
+ .description('Connect to an MCP server and start a named @session')
279
326
  .option('-H, --header <header>', 'HTTP header (can be repeated)')
280
327
  .option('--profile <name>', 'OAuth profile to use ("default" if skipped)')
281
328
  .option('--no-profile', 'Skip OAuth profile (connect anonymously)')
@@ -286,14 +333,20 @@ Full docs: ${docsUrl}`);
286
333
  ${chalk.bold('Server formats:')}
287
334
  mcp.apify.com Remote HTTP server (https:// added automatically)
288
335
  ~/.vscode/mcp.json:puppeteer Config file entry (file:entry)
289
- `)
336
+ ~/.vscode/mcp.json Config file — connect all servers in the file
337
+
338
+ ${chalk.bold('Session name:')}
339
+ If @session is omitted, a name is auto-generated from the server hostname
340
+ (e.g. mcp.apify.com → @apify) or config entry name. If a matching session
341
+ already exists (same server URL, OAuth profile, and HTTP header names), it
342
+ is reused (restarted if not live). Header values are not compared — they
343
+ are stored securely in OS keychain.
344
+ When connecting all servers from a config file, @session cannot be specified.
345
+ ${jsonHelp('`InitializeResult` object extended with `toolNames` and `_mcpc` metadata', '`{ protocolVersion, capabilities, serverInfo, instructions?, toolNames?, _mcpc }`', `${SCHEMA_BASE}#initializeresult`)}`)
290
346
  .action(async (server, sessionName, opts, command) => {
291
347
  if (!server) {
292
348
  throw new ClientError('Missing required argument: server\n\nExample: mcpc connect mcp.apify.com @myapp');
293
349
  }
294
- if (!sessionName) {
295
- throw new ClientError('Missing required argument: @session\n\nExample: mcpc connect mcp.apify.com @myapp');
296
- }
297
350
  const globalOpts = getOptionsFromCommand(command);
298
351
  const parsed = parseServerArg(server);
299
352
  const headers = opts.header
@@ -305,6 +358,29 @@ ${chalk.bold('Server formats:')}
305
358
  throw new ClientError(`Invalid server: "${server}"\n\n` +
306
359
  `Expected a URL (e.g. mcp.apify.com) or a config file entry (e.g. ~/.vscode/mcp.json:filesystem)`);
307
360
  }
361
+ if (parsed.type === 'config-file') {
362
+ if (sessionName) {
363
+ throw new ClientError(`Cannot specify @session name when connecting all servers from a config file.\n` +
364
+ `To connect a specific entry, use: mcpc connect ${server}:<entry> ${sessionName}`);
365
+ }
366
+ await sessions.connectAllFromConfig(parsed.file, {
367
+ ...globalOpts,
368
+ ...(headers && { headers }),
369
+ ...(opts.proxy && { proxy: opts.proxy }),
370
+ ...(opts.proxyBearerToken && { proxyBearerToken: opts.proxyBearerToken }),
371
+ ...(opts.x402 && { x402: opts.x402 }),
372
+ ...(globalOpts.insecure && { insecure: true }),
373
+ });
374
+ return;
375
+ }
376
+ if (!sessionName) {
377
+ sessionName = await sessions.resolveSessionName(parsed, {
378
+ outputMode: globalOpts.outputMode,
379
+ ...(globalOpts.profile && { profile: globalOpts.profile }),
380
+ ...(headers && { headers }),
381
+ ...(globalOpts.noProfile && { noProfile: globalOpts.noProfile }),
382
+ });
383
+ }
308
384
  if (parsed.type === 'config') {
309
385
  await sessions.connectSession(parsed.entry, sessionName, {
310
386
  ...globalOpts,
@@ -331,6 +407,7 @@ ${chalk.bold('Server formats:')}
331
407
  .command('close [@session]')
332
408
  .usage('<@session>')
333
409
  .description('Close a session')
410
+ .addHelpText('after', jsonHelp('`{ sessionName, closed: true }`'))
334
411
  .action(async (sessionName, _opts, command) => {
335
412
  if (!sessionName) {
336
413
  throw new ClientError('Missing required argument: @session\n\nExample: mcpc close @myapp');
@@ -362,9 +439,24 @@ ${chalk.bold('Server formats:')}
362
439
  .usage('<server>')
363
440
  .description('Interactively login to a server using OAuth and save profile')
364
441
  .option('--profile <name>', 'Profile name (default: "default")')
365
- .option('--scope <scopes>', 'OAuth scopes to request, quoted and space-separated (e.g. --scope "read write")')
366
- .option('--client-id <id>', 'OAuth client ID (for servers without dynamic client registration)')
367
- .option('--client-secret <secret>', 'OAuth client secret (for servers without dynamic client registration)')
442
+ .option('--scope <scopes>', 'OAuth scopes to request (e.g. --scope "read write")')
443
+ .option('--client-id <id>', 'Pre-registered OAuth client ID (skips CIMD and DCR)')
444
+ .option('--client-secret <secret>', 'Pre-registered OAuth client secret (requires --client-id)')
445
+ .option('--client-metadata-url <url>', 'HTTPS URL of an OAuth CIMD to use as the Client ID')
446
+ .addHelpText('after', `
447
+ ${chalk.bold('OAuth client registration approaches:')}
448
+
449
+ 1. Pre-registration: --client-id (and optionally --client-secret).
450
+ 2. Client ID Metadata Documents (CIMD): --client-metadata-url <https-url>.
451
+ Used when the authorization server advertises
452
+ "client_id_metadata_document_supported: true".
453
+ 3. Dynamic Client Registration (DCR): default fallback when the server
454
+ exposes a "registration_endpoint". No flags required.
455
+
456
+ See https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
457
+
458
+ ${jsonHelp('Interactive prompts are written to stderr, stdout contains a clean JSON object', '`{ profile, serverUrl, scopes }`')}
459
+ `)
368
460
  .action(async (server, opts, command) => {
369
461
  if (!server) {
370
462
  throw new ClientError('Missing required argument: server\n\nExample: mcpc login mcp.apify.com');
@@ -374,14 +466,16 @@ ${chalk.bold('Server formats:')}
374
466
  scope: opts.scope,
375
467
  clientId: opts.clientId,
376
468
  clientSecret: opts.clientSecret,
469
+ clientMetadataUrl: opts.clientMetadataUrl,
377
470
  ...getOptionsFromCommand(command),
378
471
  });
379
472
  });
380
473
  program
381
474
  .command('logout [server]')
382
475
  .usage('<server>')
383
- .description('Delete an authentication profile for a server')
476
+ .description('Delete an OAuth profile for a server')
384
477
  .option('--profile <name>', 'Profile name (default: "default")')
478
+ .addHelpText('after', jsonHelp('`{ profile, serverUrl, deleted: true, affectedSessions }`'))
385
479
  .action(async (server, opts, command) => {
386
480
  if (!server) {
387
481
  throw new ClientError('Missing required argument: server\n\nExample: mcpc logout mcp.apify.com');
@@ -402,7 +496,7 @@ ${chalk.bold('Resources:')}
402
496
  all Remove all of the above
403
497
 
404
498
  Without arguments, performs safe cleanup of stale data only.
405
- `)
499
+ ${jsonHelp('`{ crashedBridges, expiredSessions, orphanedBridgeLogs, sessions, profiles, logs }`')}`)
406
500
  .action(async (resources, _opts, command) => {
407
501
  const globalOpts = getOptionsFromCommand(command);
408
502
  const VALID_CLEAN_TYPES = ['sessions', 'profiles', 'logs', 'all'];
@@ -443,7 +537,7 @@ ${chalk.bold('Examples:')}
443
537
  mcpc @apify grep "actor" Search within a single session
444
538
  mcpc grep "file" --json JSON output for scripting
445
539
  mcpc grep "actor" -m 5 Show at most 5 results
446
- `)
540
+ ${jsonHelp('`[{ sessionName, tools?: Tool[], resources?: Resource[], prompts?: Prompt[], instructions?: string[] }]`')}`)
447
541
  .action(async (pattern, opts, command) => {
448
542
  if (!pattern) {
449
543
  throw new ClientError('Missing required argument: pattern\n\nUsage: mcpc grep <pattern>\n\nExample: mcpc grep "search"');
@@ -481,17 +575,12 @@ ${chalk.bold('Examples:')}
481
575
  }
482
576
  const topLevelCmd = program.commands.find((c) => c.name() === cmdName || c.aliases().includes(cmdName));
483
577
  if (topLevelCmd) {
578
+ tuneCommandHelp(topLevelCmd);
484
579
  topLevelCmd.outputHelp();
485
580
  return;
486
581
  }
487
- const dummyProgram = new Command();
488
- dummyProgram.name('mcpc <@session>');
489
- registerSessionCommands(dummyProgram, '@dummy');
490
- const sessionCmd = dummyProgram.commands.find((c) => c.name() === cmdName || c.aliases().includes(cmdName));
491
- if (sessionCmd) {
492
- sessionCmd.outputHelp();
582
+ if (showSessionCommandHelp(cmdName))
493
583
  return;
494
- }
495
584
  console.error(`Unknown command: ${cmdName}`);
496
585
  console.error(`Run "mcpc --help" for usage information.`);
497
586
  process.exit(1);
@@ -508,57 +597,150 @@ ${chalk.bold('Examples:')}
508
597
  });
509
598
  return program;
510
599
  }
600
+ const NO_JSON_COMMANDS = new Set(['shell']);
601
+ function tuneCommandHelp(cmd) {
602
+ if (!NO_JSON_COMMANDS.has(cmd.name()) && !cmd.options.some((o) => o.long === '--json')) {
603
+ cmd.option('--json', 'Output in JSON format');
604
+ }
605
+ cmd.helpOption('-h, --help', 'Display help');
606
+ const helpOpt = cmd._getHelpOption?.();
607
+ if (helpOpt)
608
+ helpOpt.hidden = true;
609
+ }
610
+ function showSessionCommandHelp(cmdName) {
611
+ const dummyProgram = createSessionProgram();
612
+ registerSessionCommands(dummyProgram, '<@session>');
613
+ for (const cmd of dummyProgram.commands) {
614
+ tuneCommandHelp(cmd);
615
+ }
616
+ const sessionCmd = dummyProgram.commands.find((c) => c.name() === cmdName || c.aliases().includes(cmdName));
617
+ if (sessionCmd) {
618
+ sessionCmd.outputHelp();
619
+ return true;
620
+ }
621
+ return false;
622
+ }
511
623
  function registerSessionCommands(program, session) {
512
624
  program
513
- .command('help')
514
- .description('Show server instructions and available capabilities')
515
- .action(async (_options, command) => {
516
- await sessions.showHelp(session, getOptionsFromCommand(command));
625
+ .command('help', { hidden: true })
626
+ .description('Show available commands and options.')
627
+ .action((_options, command) => {
628
+ command.parent.outputHelp();
517
629
  });
518
630
  program
519
631
  .command('shell')
520
- .description('Interactive shell for the session')
632
+ .description('Launch interactive MCP shell.')
521
633
  .action(async () => {
522
634
  await sessions.openShell(session);
523
635
  });
524
636
  program
525
- .command('close', { hidden: true })
526
- .description('Close the session')
637
+ .command('close')
638
+ .description('Close MCP session.')
527
639
  .action(async (_options, command) => {
528
640
  await sessions.closeSession(session, getOptionsFromCommand(command));
529
641
  });
530
642
  program
531
643
  .command('restart')
532
- .description('Restart the session (stop and start the bridge)')
644
+ .description('Restart MCP session (losing all state).')
533
645
  .action(async (_options, command) => {
534
646
  await sessions.restartSession(session, getOptionsFromCommand(command));
535
647
  });
648
+ program
649
+ .command('grep <pattern>')
650
+ .usage('<pattern> [options]')
651
+ .description('Search MCP session objects.')
652
+ .option('--tools', 'Search tools')
653
+ .option('--resources', 'Search resources')
654
+ .option('--prompts', 'Search prompts')
655
+ .option('--instructions', 'Search server instructions')
656
+ .option('-E, --regex', 'Treat pattern as a regular expression')
657
+ .option('-s, --case-sensitive', 'Case-sensitive matching')
658
+ .option('-m, --max-results <n>', 'Limit the number of results')
659
+ .addHelpText('after', `
660
+ ${chalk.bold('Type filters:')}
661
+ By default, tools and instructions are searched. Use --resources or --prompts
662
+ to search those instead. Combine flags to search multiple types.
663
+
664
+ ${chalk.bold('Examples:')}
665
+ mcpc ${session} grep "search" Search tools and instructions
666
+ mcpc ${session} grep "search" --resources Search resources only
667
+ mcpc ${session} grep "search|find" -E Regex search
668
+ ${jsonHelp('`{ tools?: Tool[], resources?: Resource[], prompts?: Prompt[], instructions?: string[] }`')}`)
669
+ .action(async (pattern, opts, command) => {
670
+ const globalOpts = getOptionsFromCommand(command);
671
+ const maxResults = opts.maxResults ? parseInt(opts.maxResults, 10) : undefined;
672
+ const exitCode = await grepCmd.grepSession(session, pattern, {
673
+ tools: opts.tools,
674
+ resources: opts.resources,
675
+ prompts: opts.prompts,
676
+ instructions: opts.instructions,
677
+ regex: opts.regex,
678
+ caseSensitive: opts.caseSensitive,
679
+ maxResults,
680
+ ...globalOpts,
681
+ });
682
+ process.exit(exitCode);
683
+ });
536
684
  program
537
685
  .command('tools')
538
- .description('List available tools (shorthand for tools-list)')
539
- .option('--full', 'Show full tool details including complete input schema')
686
+ .description('List MCP tools (shorthand for tools-list).')
687
+ .option('--full', 'Show full tool details including schema')
688
+ .addHelpText('after', jsonHelp('Array of `Tool` objects', '`[{ name, description?, inputSchema, annotations? }, ...]`', `${SCHEMA_BASE}#tool`))
540
689
  .action(async (_options, command) => {
541
690
  await tools.listTools(session, getOptionsFromCommand(command));
542
691
  });
543
692
  program
544
693
  .command('tools-list')
545
- .description('List available tools')
546
- .option('--full', 'Show full tool details including complete input schema')
694
+ .description('List all MCP tools.')
695
+ .option('--full', 'Show full tool details including schema')
696
+ .addHelpText('after', jsonHelp('Array of `Tool` objects', '`[{ name, description?, inputSchema, annotations? }, ...]`', `${SCHEMA_BASE}#tool`))
547
697
  .action(async (_options, command) => {
548
698
  await tools.listTools(session, getOptionsFromCommand(command));
549
699
  });
550
700
  program
551
701
  .command('tools-get <name>')
552
- .description('Get information about a specific tool')
702
+ .description('Get details and schema for an MCP tool.')
703
+ .option('--schema <file>', 'Validate tool schema against expected schema')
704
+ .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
705
+ .addHelpText('after', `
706
+ ${chalk.bold('Schema validation:')}
707
+ --schema <file> Validate against expected schema (save with tools-get --json)
708
+ --schema-mode <mode> strict | compatible (default) | ignore
709
+ ${jsonHelp('`Tool` object', '`{ name, description?, inputSchema, annotations? }`', `${SCHEMA_BASE}#tool`)}`)
553
710
  .action(async (name, _options, command) => {
554
711
  await tools.getTool(session, name, getOptionsFromCommand(command));
555
712
  });
713
+ const toolsCallJsonHelp = jsonHelp('`CallToolResult` object', '`{ content: [{ type, text?, ... }], isError?, structuredContent?: { ... } }`', `${SCHEMA_BASE}#calltoolresult`);
556
714
  program
557
715
  .command('tools-call <name> [args...]')
558
- .description('Call a tool with arguments (key:=value pairs or JSON)')
559
- .option('--task', 'Use task execution (experimental)')
716
+ .description('Call an MCP tool with arguments.')
717
+ .helpOption(false)
718
+ .option('--task', 'Use async task execution (experimental)')
560
719
  .option('--detach', 'Start task and return immediately with task ID (implies --task)')
720
+ .option('--schema <file>', 'Validate tool schema against expected schema before calling')
721
+ .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
722
+ .addHelpText('after', `
723
+ ${chalk.bold('Arguments:')}
724
+ key:=value pairs mcpc ${session} tools-call search query:=hello limit:=10
725
+ Inline JSON mcpc ${session} tools-call search '{"query":"hello"}'
726
+ Stdin pipe echo '{"query":"hello"}' | mcpc ${session} tools-call search
727
+
728
+ Values are auto-parsed: strings, numbers, booleans, JSON objects/arrays.
729
+ To force a string, wrap in quotes: id:='"123"'
730
+
731
+ ${chalk.bold('Schema validation:')}
732
+ --schema <file> Validate tool schema before calling (save with tools-get --json)
733
+ --schema-mode <mode> strict | compatible (default) | ignore
734
+ ${toolsCallJsonHelp}`)
561
735
  .action(async (name, args, options, command) => {
736
+ if (name === '--help' || name === '-h') {
737
+ command.help();
738
+ return;
739
+ }
740
+ if (args.includes('--help') || args.includes('-h')) {
741
+ await tools.getTool(session, name, getOptionsFromCommand(command));
742
+ return;
743
+ }
562
744
  await tools.callTool(session, name, {
563
745
  args,
564
746
  task: options.task,
@@ -568,39 +750,52 @@ function registerSessionCommands(program, session) {
568
750
  });
569
751
  program
570
752
  .command('tasks-list')
571
- .description('List active tasks')
753
+ .description('List all MCP tasks.')
754
+ .addHelpText('after', jsonHelp('`{ tasks: Task[] }`', '`{ tasks: [{ taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage?, pollInterval? }] }`', `${SCHEMA_BASE}#task`))
572
755
  .action(async (_options, command) => {
573
756
  await tasks.listTasks(session, getOptionsFromCommand(command));
574
757
  });
575
758
  program
576
759
  .command('tasks-get <taskId>')
577
- .description('Get status of a specific task')
760
+ .description('Get MCP task status.')
761
+ .addHelpText('after', jsonHelp('`Task` object', '`{ taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage?, pollInterval? }`', `${SCHEMA_BASE}#task`))
578
762
  .action(async (taskId, _options, command) => {
579
763
  await tasks.getTask(session, taskId, getOptionsFromCommand(command));
580
764
  });
765
+ program
766
+ .command('tasks-result <taskId>')
767
+ .description('Get MCP task final result (blocks until task reaches a terminal state).')
768
+ .addHelpText('after', toolsCallJsonHelp)
769
+ .action(async (taskId, _options, command) => {
770
+ await tasks.getTaskResult(session, taskId, getOptionsFromCommand(command));
771
+ });
581
772
  program
582
773
  .command('tasks-cancel <taskId>')
583
- .description('Cancel a running task')
774
+ .description('Cancel an MCP task.')
775
+ .addHelpText('after', jsonHelp('`Task` object', '`{ taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage?, pollInterval? }`', `${SCHEMA_BASE}#task`))
584
776
  .action(async (taskId, _options, command) => {
585
777
  await tasks.cancelTask(session, taskId, getOptionsFromCommand(command));
586
778
  });
587
779
  program
588
780
  .command('resources')
589
- .description('List available resources (shorthand for resources-list)')
781
+ .description('List MCP resources (shorthand for resources-list).')
782
+ .addHelpText('after', jsonHelp('Array of `Resource` objects', '`[{ uri, name?, description?, mimeType? }, ...]`', `${SCHEMA_BASE}#resource`))
590
783
  .action(async (_options, command) => {
591
784
  await resources.listResources(session, getOptionsFromCommand(command));
592
785
  });
593
786
  program
594
787
  .command('resources-list')
595
- .description('List available resources')
788
+ .description('List all MCP resources.')
789
+ .addHelpText('after', jsonHelp('Array of `Resource` objects', '`[{ uri, name?, description?, mimeType? }, ...]`', `${SCHEMA_BASE}#resource`))
596
790
  .action(async (_options, command) => {
597
791
  await resources.listResources(session, getOptionsFromCommand(command));
598
792
  });
599
793
  program
600
794
  .command('resources-read <uri>')
601
- .description('Get a resource by URI')
795
+ .description('Read an MCP resource by URI.')
602
796
  .option('-o, --output <file>', 'Write resource to file')
603
797
  .option('--max-size <bytes>', 'Maximum resource size in bytes')
798
+ .addHelpText('after', jsonHelp('`ReadResourceResult` object', '`{ contents: [{ uri, mimeType?, text? | blob? }] }`', `${SCHEMA_BASE}#readresourceresult`))
604
799
  .action(async (uri, options, command) => {
605
800
  await resources.getResource(session, uri, {
606
801
  output: options.output,
@@ -610,37 +805,51 @@ function registerSessionCommands(program, session) {
610
805
  });
611
806
  program
612
807
  .command('resources-subscribe <uri>')
613
- .description('Subscribe to resource updates')
808
+ .description('Subscribe to MCP resource updates.')
809
+ .addHelpText('after', jsonHelp('`{ subscribed: true, uri: string }`'))
614
810
  .action(async (uri, _options, command) => {
615
811
  await resources.subscribeResource(session, uri, getOptionsFromCommand(command));
616
812
  });
617
813
  program
618
814
  .command('resources-unsubscribe <uri>')
619
- .description('Unsubscribe from resource updates')
815
+ .description('Unsubscribe from MCP resource updates.')
816
+ .addHelpText('after', jsonHelp('`{ unsubscribed: true, uri: string }`'))
620
817
  .action(async (uri, _options, command) => {
621
818
  await resources.unsubscribeResource(session, uri, getOptionsFromCommand(command));
622
819
  });
623
820
  program
624
821
  .command('resources-templates-list')
625
- .description('List available resource templates')
822
+ .description('List MCP resource templates.')
823
+ .addHelpText('after', jsonHelp('Array of `ResourceTemplate` objects', '`[{ uriTemplate, name?, description?, mimeType? }, ...]`', `${SCHEMA_BASE}#resourcetemplate`))
626
824
  .action(async (_options, command) => {
627
825
  await resources.listResourceTemplates(session, getOptionsFromCommand(command));
628
826
  });
629
827
  program
630
828
  .command('prompts')
631
- .description('List available prompts (shorthand for prompts-list)')
829
+ .description('List MCP prompts (shorthand for prompts-list).')
830
+ .addHelpText('after', jsonHelp('Array of `Prompt` objects', '`[{ name, description?, arguments?: [{ name, required? }] }, ...]`', `${SCHEMA_BASE}#prompt`))
632
831
  .action(async (_options, command) => {
633
832
  await prompts.listPrompts(session, getOptionsFromCommand(command));
634
833
  });
635
834
  program
636
835
  .command('prompts-list')
637
- .description('List available prompts')
836
+ .description('List all MCP prompts.')
837
+ .addHelpText('after', jsonHelp('Array of `Prompt` objects', '`[{ name, description?, arguments?: [{ name, required? }] }, ...]`', `${SCHEMA_BASE}#prompt`))
638
838
  .action(async (_options, command) => {
639
839
  await prompts.listPrompts(session, getOptionsFromCommand(command));
640
840
  });
641
841
  program
642
842
  .command('prompts-get <name> [args...]')
643
- .description('Get a prompt by name with arguments (key:=value pairs or JSON)')
843
+ .description('Get an MCP prompt with arguments.')
844
+ .addHelpText('after', `
845
+ ${chalk.bold('Arguments:')}
846
+ key:=value pairs mcpc ${session} prompts-get summarize style:=brief lang:=en
847
+ Inline JSON mcpc ${session} prompts-get summarize '{"style":"brief"}'
848
+ Stdin pipe echo '{"style":"brief"}' | mcpc ${session} prompts-get summarize
849
+
850
+ Values are auto-parsed: strings, numbers, booleans, JSON objects/arrays.
851
+ To force a string, wrap in quotes: id:='"123"'
852
+ ${jsonHelp('`GetPromptResult` object', '`{ description?, messages: [{ role, content: { type, text?, ... } }] }`', `${SCHEMA_BASE}#getpromptresult`)}`)
644
853
  .action(async (name, args, _options, command) => {
645
854
  await prompts.getPrompt(session, name, {
646
855
  args,
@@ -649,63 +858,47 @@ function registerSessionCommands(program, session) {
649
858
  });
650
859
  program
651
860
  .command('logging-set-level <level>')
652
- .description('Set server logging level (debug, info, notice, warning, error, critical, alert, emergency)')
861
+ .description('Set MCP server logging level.')
862
+ .addHelpText('after', jsonHelp('`{ level: string }`'))
653
863
  .action(async (level, _options, command) => {
654
864
  await logging.setLogLevel(session, level, getOptionsFromCommand(command));
655
865
  });
656
866
  program
657
867
  .command('ping')
658
- .description('Ping the MCP server to check if it is alive')
868
+ .description('Ping the MCP server.')
869
+ .addHelpText('after', jsonHelp('`{ success: true, durationMs: number }`'))
659
870
  .action(async (_options, command) => {
660
871
  await utilities.ping(session, getOptionsFromCommand(command));
661
872
  });
662
- program
663
- .command('grep <pattern>')
664
- .description('Search tools and instructions')
665
- .option('--tools', 'Search tools')
666
- .option('--resources', 'Search resources')
667
- .option('--prompts', 'Search prompts')
668
- .option('--instructions', 'Search server instructions')
669
- .option('-E, --regex', 'Treat pattern as a regular expression')
670
- .option('-s, --case-sensitive', 'Case-sensitive matching')
671
- .option('-m, --max-results <n>', 'Limit the number of results')
672
- .action(async (pattern, opts, command) => {
673
- const globalOpts = getOptionsFromCommand(command);
674
- const maxResults = opts.maxResults ? parseInt(opts.maxResults, 10) : undefined;
675
- const exitCode = await grepCmd.grepSession(session, pattern, {
676
- tools: opts.tools,
677
- resources: opts.resources,
678
- prompts: opts.prompts,
679
- instructions: opts.instructions,
680
- regex: opts.regex,
681
- caseSensitive: opts.caseSensitive,
682
- maxResults,
683
- ...globalOpts,
684
- });
685
- process.exit(exitCode);
686
- });
687
873
  }
688
874
  function createSessionProgram() {
689
875
  const program = new Command();
690
876
  program.configureOutput({
691
- outputError: (str, write) => write(str),
877
+ outputError: () => { },
692
878
  getOutHelpWidth: () => 100,
693
879
  getErrHelpWidth: () => 100,
694
880
  });
881
+ program.configureHelp({
882
+ subcommandTerm: (cmd) => `${cmd.name()} ${cmd.usage()}`.replace(/^\[options\]\s*|\s*\[options\]/g, '').trim(),
883
+ styleTitle: (str) => chalk.bold(str),
884
+ styleSubcommandText: (str) => chalk.cyan(str),
885
+ });
695
886
  program
696
887
  .name('mcpc <@session>')
888
+ .description('Execute MCP commands on a connected session.')
697
889
  .helpOption('-h, --help', 'Display help')
698
- .option('-j, --json', 'Output in JSON format for scripting and code mode')
890
+ .option('--json', 'Output in JSON format for scripting and code mode')
699
891
  .option('--verbose', 'Enable debug logging')
700
892
  .option('--profile <name>', 'OAuth profile override')
701
- .option('--schema <file>', 'Validate tool/prompt schema against expected schema')
702
- .option('--schema-mode <mode>', 'Schema validation mode: strict, compatible (default), ignore')
703
893
  .option('--timeout <seconds>', 'Request timeout in seconds (default: 300)')
704
- .option('--insecure', 'Skip TLS certificate verification (for self-signed certs)');
894
+ .option('--max-chars <n>', 'Truncate output to n characters (ignored in --json mode)')
895
+ .option('--insecure', 'Skip TLS certificate verification (for self-signed certs)')
896
+ .addHelpText('after', `\nWhen no command is given, shows server info, capabilities, and tools.\n`);
705
897
  return program;
706
898
  }
707
899
  async function handleSessionCommands(session, args) {
708
- if (!hasSubcommand(args)) {
900
+ const argsSlice = args.slice(2);
901
+ if (!hasSubcommand(args) && !argsSlice.includes('--help') && !argsSlice.includes('-h')) {
709
902
  const options = extractOptions(args);
710
903
  if (options.verbose)
711
904
  setVerbose(true);
@@ -719,13 +912,37 @@ async function handleSessionCommands(session, args) {
719
912
  return;
720
913
  }
721
914
  const program = createSessionProgram();
915
+ program.exitOverride();
722
916
  registerSessionCommands(program, session);
917
+ for (const cmd of program.commands) {
918
+ tuneCommandHelp(cmd);
919
+ }
723
920
  try {
724
921
  await program.parseAsync(args);
725
922
  }
726
923
  catch (error) {
727
924
  const opts = program.opts();
728
925
  const outputMode = opts.json ? 'json' : 'human';
926
+ if (error instanceof CommanderError && error.code === 'commander.unknownCommand') {
927
+ const unknownCmd = args.find((a, i) => i >= 2 && !a.startsWith('-') && !KNOWN_SESSION_COMMANDS.includes(a));
928
+ if (unknownCmd) {
929
+ const suggestion = suggestCommand(unknownCmd, KNOWN_SESSION_COMMANDS);
930
+ if (outputMode === 'json') {
931
+ console.error(formatJsonError(new Error(`Unknown command: ${unknownCmd}`), 1));
932
+ }
933
+ else {
934
+ console.error(`Error: Unknown command: ${unknownCmd}`);
935
+ if (suggestion) {
936
+ console.error(`\nDid you mean: mcpc ${session} ${suggestion}`);
937
+ }
938
+ console.error(`Run "mcpc ${session} --help" for available commands.\n`);
939
+ }
940
+ process.exit(1);
941
+ }
942
+ }
943
+ if (error instanceof CommanderError && error.code === 'commander.helpDisplayed') {
944
+ process.exit(0);
945
+ }
729
946
  if (isMcpError(error)) {
730
947
  if (outputMode === 'json') {
731
948
  console.error(formatJsonError(error, error.code));